diff --git a/src/graphics/niche/InkHUD/Applet.h b/src/graphics/niche/InkHUD/Applet.h index 802186e6e3..0a152a5be4 100644 --- a/src/graphics/niche/InkHUD/Applet.h +++ b/src/graphics/niche/InkHUD/Applet.h @@ -90,6 +90,12 @@ class Applet : public GFX virtual void onShutdown() {} virtual void onButtonShortPress() {} // (System Applets only) virtual void onButtonLongPress() {} // (System Applets only) + virtual void onStickCenterShort() {} // (System Applets only) + virtual void onStickCenterLong() {} // (System Applets only) + virtual void onStickUp() {} // (System Applets only) + virtual void onStickDown() {} // (System Applets only) + virtual void onStickLeft() {} // (System Applets only) + virtual void onStickRight() {} // (System Applets only) virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification diff --git a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp new file mode 100644 index 0000000000..b73635e702 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp @@ -0,0 +1,205 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +#include "./AlignStickApplet.h" + +using namespace NicheGraphics; + +InkHUD::AlignStickApplet::AlignStickApplet() +{ + if (!settings->joystick.aligned) + bringToForeground(); +} + +void InkHUD::AlignStickApplet::onRender() +{ + setFont(fontMedium); + printAt(0, 0, "Align Joystick:"); + setFont(fontSmall); + std::string instructions = "Move joystick in the direction indicated"; + printWrapped(0, fontMedium.lineHeight() * 1.5, width(), instructions); + + // Size of the region in which the joystick graphic should fit + uint16_t joyXLimit = X(0.8); + uint16_t contentH = fontMedium.lineHeight() * 1.5 + fontSmall.lineHeight() * 1; + if (getTextWidth(instructions) > width()) + contentH += fontSmall.lineHeight(); + uint16_t freeY = height() - contentH - fontSmall.lineHeight() * 1.2; + uint16_t joyYLimit = freeY * 0.8; + + // Use the shorter of the two + uint16_t joyWidth = joyXLimit < joyYLimit ? joyXLimit : joyYLimit; + + // Center the joystick graphic + uint16_t centerX = X(0.5); + uint16_t centerY = contentH + freeY * 0.5; + + // Draw joystick graphic + drawStick(centerX, centerY, joyWidth); + + setFont(fontSmall); + printAt(X(0.5), Y(1.0) - fontSmall.lineHeight() * 0.2, "Long press to skip", CENTER, BOTTOM); +} + +// Draw a scalable joystick graphic +void InkHUD::AlignStickApplet::drawStick(uint16_t centerX, uint16_t centerY, uint16_t width) +{ + if (width < 9) // too small to draw + return; + + else if (width < 40) { // only draw up arrow + uint16_t chamfer = width < 20 ? 1 : 2; + + // Draw filled up arrow + drawDirection(centerX, centerY - width / 4, Direction::UP, width, chamfer, BLACK); + + } else { // large enough to draw the full thing + uint16_t chamfer = width < 80 ? 1 : 2; + uint16_t stroke = 3; // pixels + uint16_t arrowW = width * 0.22; + uint16_t hollowW = arrowW - stroke * 2; + + // Draw center circle + fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2), BLACK); + fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2) - stroke, WHITE); + + // Draw filled up arrow + drawDirection(centerX, centerY - width / 2, Direction::UP, arrowW, chamfer, BLACK); + + // Draw down arrow + drawDirection(centerX, centerY + width / 2, Direction::DOWN, arrowW, chamfer, BLACK); + drawDirection(centerX, centerY + width / 2 - stroke, Direction::DOWN, hollowW, 0, WHITE); + + // Draw left arrow + drawDirection(centerX - width / 2, centerY, Direction::LEFT, arrowW, chamfer, BLACK); + drawDirection(centerX - width / 2 + stroke, centerY, Direction::LEFT, hollowW, 0, WHITE); + + // Draw right arrow + drawDirection(centerX + width / 2, centerY, Direction::RIGHT, arrowW, chamfer, BLACK); + drawDirection(centerX + width / 2 - stroke, centerY, Direction::RIGHT, hollowW, 0, WHITE); + } +} + +// Draw a scalable joystick direction arrow +// a right-triangle with blunted tips +/* + _ <--point + ^ / \ + | / \ + size / \ + | / \ + v |_________| + +*/ +void InkHUD::AlignStickApplet::drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size, + uint16_t chamfer, Color color) +{ + uint16_t chamferW = chamfer * 2 + 1; + uint16_t triangleW = size - chamferW; + + // Draw arrow + switch (direction) { + case Direction::UP: + fillRect(pointX - chamfer, pointY, chamferW, triangleW, color); + fillRect(pointX - chamfer - triangleW, pointY + triangleW, chamferW + triangleW * 2, chamferW, color); + fillTriangle(pointX - chamfer, pointY, pointX - chamfer - triangleW, pointY + triangleW, pointX - chamfer, + pointY + triangleW, color); + fillTriangle(pointX + chamfer, pointY, pointX + chamfer + triangleW, pointY + triangleW, pointX + chamfer, + pointY + triangleW, color); + break; + case Direction::DOWN: + fillRect(pointX - chamfer, pointY - triangleW + 1, chamferW, triangleW, color); + fillRect(pointX - chamfer - triangleW, pointY - size + 1, chamferW + triangleW * 2, chamferW, color); + fillTriangle(pointX - chamfer, pointY, pointX - chamfer - triangleW, pointY - triangleW, pointX - chamfer, + pointY - triangleW, color); + fillTriangle(pointX + chamfer, pointY, pointX + chamfer + triangleW, pointY - triangleW, pointX + chamfer, + pointY - triangleW, color); + break; + case Direction::LEFT: + fillRect(pointX, pointY - chamfer, triangleW, chamferW, color); + fillRect(pointX + triangleW, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, color); + fillTriangle(pointX, pointY - chamfer, pointX + triangleW, pointY - chamfer - triangleW, pointX + triangleW, + pointY - chamfer, color); + fillTriangle(pointX, pointY + chamfer, pointX + triangleW, pointY + chamfer + triangleW, pointX + triangleW, + pointY + chamfer, color); + break; + case Direction::RIGHT: + fillRect(pointX - triangleW + 1, pointY - chamfer, triangleW, chamferW, color); + fillRect(pointX - size + 1, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, color); + fillTriangle(pointX, pointY - chamfer, pointX - triangleW, pointY - chamfer - triangleW, pointX - triangleW, + pointY - chamfer, color); + fillTriangle(pointX, pointY + chamfer, pointX - triangleW, pointY + chamfer + triangleW, pointX - triangleW, + pointY + chamfer, color); + break; + } +} + +void InkHUD::AlignStickApplet::onForeground() +{ + // Prevent most other applets from requesting update, and skip their rendering entirely + // Another system applet with a higher precedence can potentially ignore this + SystemApplet::lockRendering = true; + SystemApplet::lockRequests = true; + + handleInput = true; // Intercept the button input for our applet +} + +void InkHUD::AlignStickApplet::onBackground() +{ + // Allow normal update behavior to resume + SystemApplet::lockRendering = false; + SystemApplet::lockRequests = false; + SystemApplet::handleInput = false; + + // Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background + // Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +void InkHUD::AlignStickApplet::onButtonLongPress() +{ + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +void InkHUD::AlignStickApplet::onStickCenterLong() +{ + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +void InkHUD::AlignStickApplet::onStickUp() +{ + settings->joystick.aligned = true; + + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +void InkHUD::AlignStickApplet::onStickDown() +{ + inkhud->rotateJoystick(2); // 180 deg + settings->joystick.aligned = true; + + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +void InkHUD::AlignStickApplet::onStickLeft() +{ + inkhud->rotateJoystick(3); // 270 deg + settings->joystick.aligned = true; + + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +void InkHUD::AlignStickApplet::onStickRight() +{ + inkhud->rotateJoystick(1); // 90 deg + settings->joystick.aligned = true; + + sendToBackground(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h new file mode 100644 index 0000000000..327025f343 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h @@ -0,0 +1,50 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +System Applet for manually aligning the joystick with the screen + +should be run at startup if the joystick is enabled +and not aligned to the screen + +*/ + +#pragma once + +#include "configuration.h" + +#include "graphics/niche/InkHUD/SystemApplet.h" + +namespace NicheGraphics::InkHUD +{ + +class AlignStickApplet : public SystemApplet +{ + public: + AlignStickApplet(); + + void onRender() override; + void onForeground() override; + void onBackground() override; + void onButtonLongPress() override; + void onStickCenterLong() override; + void onStickUp() override; + void onStickDown() override; + void onStickLeft() override; + void onStickRight() override; + + protected: + enum Direction { + UP, + DOWN, + LEFT, + RIGHT, + }; + + void drawStick(uint16_t centerX, uint16_t centerY, uint16_t width); + void drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size, uint16_t chamfer, Color color); +}; + +} // namespace NicheGraphics::InkHUD + +#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h index c84ee09e03..debe2b719c 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h @@ -30,6 +30,7 @@ enum MenuAction { TOGGLE_AUTOSHOW_APPLET, SET_RECENTS, ROTATE, + ALIGN_JOYSTICK, LAYOUT, TOGGLE_BATTERY_ICON, TOGGLE_NOTIFICATIONS, diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp index 09f76ed461..73e757acb4 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp @@ -178,6 +178,10 @@ void InkHUD::MenuApplet::execute(MenuItem item) inkhud->rotate(); break; + case ALIGN_JOYSTICK: + inkhud->openAlignStick(); + break; + case LAYOUT: // Todo: smarter incrementing of tile count settings->userTiles.count++; @@ -287,14 +291,17 @@ void InkHUD::MenuApplet::showPage(MenuPage page) // items.push_back(MenuItem("Display Off", MenuPage::EXIT)); // TODO items.push_back(MenuItem("Save & Shut Down", MenuAction::SHUTDOWN)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); + previousPage = MenuPage::EXIT; break; case SEND: populateSendPage(); + previousPage = MenuPage::ROOT; break; case CANNEDMESSAGE_RECIPIENT: populateRecipientPage(); + previousPage = MenuPage::OPTIONS; break; case OPTIONS: @@ -321,6 +328,8 @@ void InkHUD::MenuApplet::showPage(MenuPage page) if (settings->userTiles.maxCount > 1) items.push_back(MenuItem("Layout", MenuAction::LAYOUT, MenuPage::OPTIONS)); items.push_back(MenuItem("Rotate", MenuAction::ROTATE, MenuPage::OPTIONS)); + if (settings->joystick.enabled) + items.push_back(MenuItem("Align Joystick", MenuAction::ALIGN_JOYSTICK, MenuPage::EXIT)); items.push_back(MenuItem("Notifications", MenuAction::TOGGLE_NOTIFICATIONS, MenuPage::OPTIONS, &settings->optionalFeatures.notifications)); items.push_back(MenuItem("Battery Icon", MenuAction::TOGGLE_BATTERY_ICON, MenuPage::OPTIONS, @@ -332,20 +341,24 @@ void InkHUD::MenuApplet::showPage(MenuPage page) items.push_back( MenuItem("12-Hour Clock", MenuAction::TOGGLE_12H_CLOCK, MenuPage::OPTIONS, &config.display.use_12h_clock)); items.push_back(MenuItem("Exit", MenuPage::EXIT)); + previousPage = MenuPage::ROOT; break; case APPLETS: populateAppletPage(); items.push_back(MenuItem("Exit", MenuPage::EXIT)); + previousPage = MenuPage::OPTIONS; break; case AUTOSHOW: populateAutoshowPage(); items.push_back(MenuItem("Exit", MenuPage::EXIT)); + previousPage = MenuPage::OPTIONS; break; case RECENTS: populateRecentsPage(); + previousPage = MenuPage::OPTIONS; break; case EXIT: @@ -476,7 +489,88 @@ void InkHUD::MenuApplet::onRender() void InkHUD::MenuApplet::onButtonShortPress() { - // Push the auto-close timer back + if (!settings->joystick.enabled) { + // Push the auto-close timer back + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + // Move menu cursor to next entry, then update + if (cursorShown) + cursor = (cursor + 1) % items.size(); + else + cursorShown = true; + requestUpdate(Drivers::EInk::UpdateTypes::FAST); + } else { + // Exit the menu + showPage(MenuPage::EXIT); + + requestUpdate(Drivers::EInk::UpdateTypes::FAST); + } +} + +void InkHUD::MenuApplet::onButtonLongPress() +{ + if (!settings->joystick.enabled) { + // Push the auto-close timer back + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + if (cursorShown) + execute(items.at(cursor)); + else + showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close + + // If we didn't already request a specialized update, when handling a menu action, + // then perform the usual fast update. + // FAST keeps things responsive: important because we're dealing with user input + if (!wantsToRender()) + requestUpdate(Drivers::EInk::UpdateTypes::FAST); + } else { + // Exit the menu + showPage(MenuPage::EXIT); + + requestUpdate(Drivers::EInk::UpdateTypes::FAST); + } +} + +// The center button of the joystick takes over the role of the a long press on the user button +void InkHUD::MenuApplet::onStickCenterShort() +{ + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + if (cursorShown) + execute(items.at(cursor)); + else + showPage(MenuPage::EXIT); + + if (!wantsToRender()) + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onStickCenterLong() +{ + // Exit the menu + showPage(MenuPage::EXIT); + + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onStickUp() +{ + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + // Move menu cursor to previous entry, then update + if (cursor == 0) + cursor = items.size() - 1; + else + cursor--; + + if (!cursorShown) + cursorShown = true; + + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onStickDown() +{ OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); // Move menu cursor to next entry, then update @@ -484,22 +578,25 @@ void InkHUD::MenuApplet::onButtonShortPress() cursor = (cursor + 1) % items.size(); else cursorShown = true; + requestUpdate(Drivers::EInk::UpdateTypes::FAST); } -void InkHUD::MenuApplet::onButtonLongPress() +void InkHUD::MenuApplet::onStickLeft() +{ + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + // Go to the previous menu page + showPage(previousPage); + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onStickRight() { - // Push the auto-close timer back OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); if (cursorShown) execute(items.at(cursor)); - else - showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close - - // If we didn't already request a specialized update, when handling a menu action, - // then perform the usual fast update. - // FAST keeps things responsive: important because we're dealing with user input if (!wantsToRender()) requestUpdate(Drivers::EInk::UpdateTypes::FAST); } @@ -796,4 +893,4 @@ void InkHUD::MenuApplet::freeCannedMessageResources() cm.recipientItems.clear(); } -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h index 8f9280e6ff..64fc2e7991 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h @@ -27,6 +27,12 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread void onBackground() override; void onButtonShortPress() override; void onButtonLongPress() override; + void onStickCenterShort() override; + void onStickCenterLong() override; + void onStickUp() override; + void onStickDown() override; + void onStickLeft() override; + void onStickRight() override; void onRender() override; void show(Tile *t); // Open the menu, onto a user tile @@ -52,6 +58,7 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread void freeCannedMessageResources(); // Clear MenuApplet's canned message processing data MenuPage currentPage = MenuPage::ROOT; + MenuPage previousPage = MenuPage::EXIT; uint8_t cursor = 0; // Which menu item is currently highlighted bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection) @@ -97,4 +104,4 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread } // namespace NicheGraphics::InkHUD -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp index ae0836d198..ff11ba580e 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp @@ -153,6 +153,42 @@ void InkHUD::NotificationApplet::onButtonLongPress() inkhud->forceUpdate(EInk::UpdateTypes::FULL); } +void InkHUD::NotificationApplet::onStickCenterShort() +{ + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +void InkHUD::NotificationApplet::onStickCenterLong() +{ + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +void InkHUD::NotificationApplet::onStickUp() +{ + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +void InkHUD::NotificationApplet::onStickDown() +{ + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +void InkHUD::NotificationApplet::onStickLeft() +{ + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + +void InkHUD::NotificationApplet::onStickRight() +{ + dismiss(); + inkhud->forceUpdate(EInk::UpdateTypes::FULL); +} + // Ask the WindowManager to check whether any displayed applets are already displaying the info from this notification // Called internally when we first get a "notifiable event", and then again before render, // in case autoshow swapped which applet was displayed diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h index 66df784b40..26055d898d 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.h @@ -31,6 +31,12 @@ class NotificationApplet : public SystemApplet void onBackground() override; void onButtonShortPress() override; void onButtonLongPress() override; + void onStickCenterShort() override; + void onStickCenterLong() override; + void onStickUp() override; + void onStickDown() override; + void onStickLeft() override; + void onStickRight() override; int onReceiveTextMessage(const meshtastic_MeshPacket *p); diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp index ade44ab65e..e158b5cc26 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp @@ -112,12 +112,23 @@ void InkHUD::TipsApplet::onRender() setFont(fontSmall); int16_t cursorY = fontMedium.lineHeight() * 1.5; - printAt(0, cursorY, "User Button"); - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- short press: next"); - cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- long press: select / open menu"); - cursorY += fontSmall.lineHeight() * 1.5; + if (!settings->joystick.enabled) { + printAt(0, cursorY, "User Button"); + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- short press: next"); + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- long press: select / open menu"); + } else { + printAt(0, cursorY, "User Button"); + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- press: open / close menu"); + cursorY += fontSmall.lineHeight() * 1.5; + printAt(0, cursorY, "Joystick"); + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- short press: switch tile / select"); + cursorY += fontSmall.lineHeight() * 1.2; + printAt(0, cursorY, "- long press: open / close menu"); + } printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); } break; @@ -127,8 +138,13 @@ void InkHUD::TipsApplet::onRender() printAt(0, 0, "Tip: Rotation"); setFont(fontSmall); - printWrapped(0, fontMedium.lineHeight() * 1.5, width(), - "To rotate the display, use the InkHUD menu. Long-press the user button > Options > Rotate."); + if (!settings->joystick.enabled) { + printWrapped(0, fontMedium.lineHeight() * 1.5, width(), + "To rotate the display, use the InkHUD menu. Long-press the user button > Options > Rotate."); + } else { + printWrapped(0, fontMedium.lineHeight() * 1.5, width(), + "To rotate the display, use the InkHUD menu. Press the user button > Options > Rotate."); + } printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM); @@ -232,4 +248,10 @@ void InkHUD::TipsApplet::onButtonShortPress() requestUpdate(); } +// Functions the same as the user button +void InkHUD::TipsApplet::onStickCenterShort() +{ + onButtonShortPress(); +} + #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h index db88585e91..239d2d687e 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.h @@ -36,6 +36,7 @@ class TipsApplet : public SystemApplet void onForeground() override; void onBackground() override; void onButtonShortPress() override; + void onStickCenterShort() override; protected: void renderWelcome(); // Very first screen of tutorial diff --git a/src/graphics/niche/InkHUD/Events.cpp b/src/graphics/niche/InkHUD/Events.cpp index cdda1638db..1c17407f0d 100644 --- a/src/graphics/niche/InkHUD/Events.cpp +++ b/src/graphics/niche/InkHUD/Events.cpp @@ -55,10 +55,15 @@ void InkHUD::Events::onButtonShort() } // If no system applet is handling input, default behavior instead is to cycle applets - if (consumer) + // 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 - inkhud->nextApplet(); + } else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module + if (!settings->joystick.enabled) + inkhud->nextApplet(); + else + inkhud->openMenu(); + } } void InkHUD::Events::onButtonLong() @@ -83,6 +88,159 @@ void InkHUD::Events::onButtonLong() inkhud->openMenu(); } +void InkHUD::Events::onStickCenterShort() +{ + if (settings->joystick.enabled) { + // 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 change tiles + if (consumer) + consumer->onStickCenterShort(); + else if (!dismissedExt) // Don't change tile if this button press silenced the external notification module + inkhud->nextTile(); + } +} + +void InkHUD::Events::onStickCenterLong() +{ + if (settings->joystick.enabled) { + // 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->onStickCenterLong(); + else + inkhud->openMenu(); + } +} + +void InkHUD::Events::onStickUp() +{ + if (settings->joystick.enabled) { + // 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->onStickUp(); + } +} + +void InkHUD::Events::onStickDown() +{ + if (settings->joystick.enabled) { + // 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->onStickDown(); + } +} + +void InkHUD::Events::onStickLeft() +{ + if (settings->joystick.enabled) { + // 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->onStickLeft(); + else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module + inkhud->prevApplet(); + } +} + +void InkHUD::Events::onStickRight() +{ + if (settings->joystick.enabled) { + // 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->onStickRight(); + else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module + inkhud->nextApplet(); + } +} + // Callback for deepSleepObserver // Returns 0 to signal that we agree to sleep now int InkHUD::Events::beforeDeepSleep(void *unused) diff --git a/src/graphics/niche/InkHUD/Events.h b/src/graphics/niche/InkHUD/Events.h index df68f368cb..9aedbc9b14 100644 --- a/src/graphics/niche/InkHUD/Events.h +++ b/src/graphics/niche/InkHUD/Events.h @@ -29,6 +29,12 @@ class Events void onButtonShort(); // User button: short press void onButtonLong(); // User button: long press + void onStickCenterShort(); + void onStickCenterLong(); + void onStickUp(); + void onStickDown(); + void onStickLeft(); + void onStickRight(); int beforeDeepSleep(void *unused); // Prepare for shutdown int beforeReboot(void *unused); // Prepare for reboot diff --git a/src/graphics/niche/InkHUD/InkHUD.cpp b/src/graphics/niche/InkHUD/InkHUD.cpp index 90b6718e05..1a341b2b3f 100644 --- a/src/graphics/niche/InkHUD/InkHUD.cpp +++ b/src/graphics/niche/InkHUD/InkHUD.cpp @@ -80,6 +80,94 @@ void InkHUD::InkHUD::longpress() events->onButtonLong(); } +// Call this when your joystick gets a short center press +void InkHUD::InkHUD::stickCenterShort() +{ + events->onStickCenterShort(); +} + +// Call this when your joystick gets a long center press +void InkHUD::InkHUD::stickCenterLong() +{ + events->onStickCenterLong(); +} + +// Call this when your joystick gets an up input +void InkHUD::InkHUD::stickUp() +{ + switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { + case 1: // 90 deg + events->onStickLeft(); + break; + case 2: // 180 deg + events->onStickDown(); + break; + case 3: // 270 deg + events->onStickRight(); + break; + default: // 0 deg + events->onStickUp(); + break; + } +} + +// Call this when your joystick gets a down input +void InkHUD::InkHUD::stickDown() +{ + switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { + case 1: // 90 deg + events->onStickRight(); + break; + case 2: // 180 deg + events->onStickUp(); + break; + case 3: // 270 deg + events->onStickLeft(); + break; + default: // 0 deg + events->onStickDown(); + break; + } +} + +// Call this when your joystick gets a left input +void InkHUD::InkHUD::stickLeft() +{ + switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { + case 1: // 90 deg + events->onStickDown(); + break; + case 2: // 180 deg + events->onStickRight(); + break; + case 3: // 270 deg + events->onStickUp(); + break; + default: // 0 deg + events->onStickLeft(); + break; + } +} + +// Call this when your joystick gets a right input +void InkHUD::InkHUD::stickRight() +{ + switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) { + case 1: // 90 deg + events->onStickUp(); + break; + case 2: // 180 deg + events->onStickLeft(); + break; + case 3: // 270 deg + events->onStickDown(); + break; + default: // 0 deg + events->onStickRight(); + break; + } +} + // 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" @@ -88,6 +176,14 @@ 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(); +} + // Show the menu (on the the focused tile) // The applet previously displayed there will be restored once the menu closes void InkHUD::InkHUD::openMenu() @@ -95,6 +191,12 @@ void InkHUD::InkHUD::openMenu() windowManager->openMenu(); } +// Bring AlignStick applet to the foreground +void InkHUD::InkHUD::openAlignStick() +{ + windowManager->openAlignStick(); +} + // 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() @@ -102,12 +204,26 @@ 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(); +} + // 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() { diff --git a/src/graphics/niche/InkHUD/InkHUD.h b/src/graphics/niche/InkHUD/InkHUD.h index 13839ea22c..da23275044 100644 --- a/src/graphics/niche/InkHUD/InkHUD.h +++ b/src/graphics/niche/InkHUD/InkHUD.h @@ -56,14 +56,25 @@ class InkHUD void shortpress(); void longpress(); + void stickCenterShort(); + void stickCenterLong(); + void stickUp(); + void stickDown(); + void stickLeft(); + void stickRight(); + // Trigger UI changes // - called by various InkHUD components // - suitable(?) for use by aux button, connected in variant nicheGraphics.h void nextApplet(); + void prevApplet(); void openMenu(); + void openAlignStick(); void nextTile(); + void prevTile(); void rotate(); + void rotateJoystick(uint8_t angle = 1); // rotate 90 deg by default void toggleBatteryIcon(); // Updating the display diff --git a/src/graphics/niche/InkHUD/Persistence.h b/src/graphics/niche/InkHUD/Persistence.h index b85274c87d..5054b7234f 100644 --- a/src/graphics/niche/InkHUD/Persistence.h +++ b/src/graphics/niche/InkHUD/Persistence.h @@ -29,7 +29,7 @@ class Persistence // Used to invalidate old settings, if needed // Version 0 is reserved for testing, and will always load defaults - static constexpr uint32_t SETTINGS_VERSION = 2; + static constexpr uint32_t SETTINGS_VERSION = 3; struct Settings { struct Meta { @@ -96,6 +96,19 @@ class Persistence bool safeShutdownSeen = false; } tips; + // Joystick settings for enabling and aligning to the screen + struct Joystick { + // Modifies the UI for joystick use + bool enabled = false; + + // gets set to true when AlignStick applet is completed + bool aligned = false; + + // Rotation of the joystick + // Multiples of 90 degrees clockwise + uint8_t alignment = 0; + } joystick; + // Rotation of the display // Multiples of 90 degrees clockwise // Most commonly: rotation is 0 when flex connector is oriented below display diff --git a/src/graphics/niche/InkHUD/WindowManager.cpp b/src/graphics/niche/InkHUD/WindowManager.cpp index c883e9a298..0548de1eb0 100644 --- a/src/graphics/niche/InkHUD/WindowManager.cpp +++ b/src/graphics/niche/InkHUD/WindowManager.cpp @@ -2,6 +2,7 @@ #include "./WindowManager.h" +#include "./Applets/System/AlignStick/AlignStickApplet.h" #include "./Applets/System/BatteryIcon/BatteryIconApplet.h" #include "./Applets/System/Logo/LogoApplet.h" #include "./Applets/System/Menu/MenuApplet.h" @@ -98,6 +99,38 @@ void InkHUD::WindowManager::nextTile() userTiles.at(settings->userTiles.focused)->requestHighlight(); } +// Focus on a different tile but decrement index +void InkHUD::WindowManager::prevTile() +{ + // Close the menu applet if open + // We don't *really* want to do this, but it simplifies handling *a lot* + MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu"); + bool menuWasOpen = false; + if (menu->isForeground()) { + menu->sendToBackground(); + menuWasOpen = true; + } + + // Swap to next tile + if (settings->userTiles.focused == 0) + settings->userTiles.focused = settings->userTiles.count - 1; + else + settings->userTiles.focused--; + + // Make sure that we don't get stuck on the placeholder tile + refocusTile(); + + if (menuWasOpen) + menu->show(userTiles.at(settings->userTiles.focused)); + + // Ask the tile to draw an indicator showing which tile is now focused + // Requests a render + // We only draw this indicator if the device uses an aux button to switch tiles. + // Assume aux button is used to switch tiles if the "next tile" menu item is hidden + if (!settings->optionalMenuItems.nextTile) + userTiles.at(settings->userTiles.focused)->requestHighlight(); +} + // Show the menu (on the the focused tile) // The applet previously displayed there will be restored once the menu closes void InkHUD::WindowManager::openMenu() @@ -106,6 +139,15 @@ void InkHUD::WindowManager::openMenu() menu->show(userTiles.at(settings->userTiles.focused)); } +// Bring the AlignStick applet to the foreground +void InkHUD::WindowManager::openAlignStick() +{ + if (settings->joystick.enabled) { + AlignStickApplet *alignStick = (AlignStickApplet *)inkhud->getSystemApplet("AlignStick"); + alignStick->bringToForeground(); + } +} + // On the currently focussed tile: cycle to the next available user applet // Applets available for this must be activated, and not already displayed on another tile void InkHUD::WindowManager::nextApplet() @@ -155,6 +197,59 @@ void InkHUD::WindowManager::nextApplet() inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST } +// On the currently focussed tile: cycle to the previous available user applet +// Applets available for this must be activated, and not already displayed on another tile +void InkHUD::WindowManager::prevApplet() +{ + Tile *t = userTiles.at(settings->userTiles.focused); + + // Abort if zero applets available + // nullptr means WindowManager::refocusTile determined that there were no available applets + if (!t->getAssignedApplet()) + return; + + // Find the index of the applet currently shown on the tile + uint8_t appletIndex = -1; + for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) { + if (inkhud->userApplets.at(i) == t->getAssignedApplet()) { + appletIndex = i; + break; + } + } + + // Confirm that we did find the applet + assert(appletIndex != (uint8_t)-1); + + // Iterate forward through the WindowManager::applets, looking for the previous valid applet + Applet *prevValidApplet = nullptr; + for (uint8_t i = 1; i < inkhud->userApplets.size(); i++) { + uint8_t newAppletIndex = 0; + if (i > appletIndex) + newAppletIndex = inkhud->userApplets.size() + appletIndex - i; + else + newAppletIndex = (appletIndex - i); + Applet *a = inkhud->userApplets.at(newAppletIndex); + + // Looking for an applet which is active (enabled by user), but currently in background + if (a->isActive() && !a->isForeground()) { + prevValidApplet = a; + settings->userTiles.displayedUserApplet[settings->userTiles.focused] = + newAppletIndex; // Remember this setting between boots! + break; + } + } + + // Confirm that we found another applet + if (!prevValidApplet) + return; + + // Hide old applet, show new applet + t->getAssignedApplet()->sendToBackground(); + t->assignApplet(prevValidApplet); + prevValidApplet->bringToForeground(); + inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST +} + // Rotate the display image by 90 degrees void InkHUD::WindowManager::rotate() { @@ -338,6 +433,8 @@ void InkHUD::WindowManager::createSystemApplets() addSystemApplet("Logo", new LogoApplet, new Tile); addSystemApplet("Pairing", new PairingApplet, new Tile); addSystemApplet("Tips", new TipsApplet, new Tile); + if (settings->joystick.enabled) + addSystemApplet("AlignStick", new AlignStickApplet, new Tile); addSystemApplet("Menu", new MenuApplet, nullptr); @@ -360,6 +457,8 @@ void InkHUD::WindowManager::placeSystemTiles() inkhud->getSystemApplet("Logo")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); inkhud->getSystemApplet("Pairing")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); inkhud->getSystemApplet("Tips")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); + if (settings->joystick.enabled) + inkhud->getSystemApplet("AlignStick")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height()); inkhud->getSystemApplet("Notification")->getTile()->setRegion(0, 0, inkhud->width(), 20); diff --git a/src/graphics/niche/InkHUD/WindowManager.h b/src/graphics/niche/InkHUD/WindowManager.h index 4d1aedf1be..5def48f8c8 100644 --- a/src/graphics/niche/InkHUD/WindowManager.h +++ b/src/graphics/niche/InkHUD/WindowManager.h @@ -28,8 +28,11 @@ class WindowManager // - call these to make stuff change void nextTile(); + void prevTile(); void openMenu(); + void openAlignStick(); void nextApplet(); + void prevApplet(); void rotate(); void toggleBatteryIcon(); diff --git a/src/graphics/niche/Inputs/TwoButtonExtended.cpp b/src/graphics/niche/Inputs/TwoButtonExtended.cpp new file mode 100644 index 0000000000..287fb943fa --- /dev/null +++ b/src/graphics/niche/Inputs/TwoButtonExtended.cpp @@ -0,0 +1,523 @@ +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "./TwoButtonExtended.h" + +#include "NodeDB.h" // For the helper function TwoButtonExtended::getUserButtonPin +#include "PowerFSM.h" +#include "sleep.h" + +using namespace NicheGraphics::Inputs; + +TwoButtonExtended::TwoButtonExtended() : concurrency::OSThread("TwoButtonExtended") +{ + // Don't start polling buttons for release immediately + // Assume they are in a "released" state at boot + OSThread::disable(); + +#ifdef ARCH_ESP32 + // Register callbacks for before and after lightsleep + lsObserver.observe(¬ifyLightSleep); + lsEndObserver.observe(¬ifyLightSleepEnd); +#endif + + // Explicitly initialize these, just to keep cppcheck quiet.. + buttons[0] = Button(); + buttons[1] = Button(); + joystick[Direction::UP] = SimpleButton(); + joystick[Direction::DOWN] = SimpleButton(); + joystick[Direction::LEFT] = SimpleButton(); + joystick[Direction::RIGHT] = SimpleButton(); +} + +// Get access to (or create) the singleton instance of this class +// Accessible inside the ISRs, even though we maybe shouldn't +TwoButtonExtended *TwoButtonExtended::getInstance() +{ + // Instantiate the class the first time this method is called + static TwoButtonExtended *const singletonInstance = new TwoButtonExtended; + + return singletonInstance; +} + +// Begin receiving button input +// We probably need to do this after sleep, as well as at boot +void TwoButtonExtended::start() +{ + if (buttons[0].pin != 0xFF) + attachInterrupt(buttons[0].pin, TwoButtonExtended::isrPrimary, buttons[0].activeLogic == LOW ? FALLING : RISING); + + if (buttons[1].pin != 0xFF) + attachInterrupt(buttons[1].pin, TwoButtonExtended::isrSecondary, buttons[1].activeLogic == LOW ? FALLING : RISING); + + if (joystick[Direction::UP].pin != 0xFF) + attachInterrupt(joystick[Direction::UP].pin, TwoButtonExtended::isrJoystickUp, + joystickActiveLogic == LOW ? FALLING : RISING); + + if (joystick[Direction::DOWN].pin != 0xFF) + attachInterrupt(joystick[Direction::DOWN].pin, TwoButtonExtended::isrJoystickDown, + joystickActiveLogic == LOW ? FALLING : RISING); + + if (joystick[Direction::LEFT].pin != 0xFF) + attachInterrupt(joystick[Direction::LEFT].pin, TwoButtonExtended::isrJoystickLeft, + joystickActiveLogic == LOW ? FALLING : RISING); + + if (joystick[Direction::RIGHT].pin != 0xFF) + attachInterrupt(joystick[Direction::RIGHT].pin, TwoButtonExtended::isrJoystickRight, + joystickActiveLogic == LOW ? FALLING : RISING); +} + +// Stop receiving button input, and run custom sleep code +// Called before device sleeps. This might be power-off, or just ESP32 light sleep +// Some devices will want to attach interrupts here, for the user button to wake from sleep +void TwoButtonExtended::stop() +{ + if (buttons[0].pin != 0xFF) + detachInterrupt(buttons[0].pin); + + if (buttons[1].pin != 0xFF) + detachInterrupt(buttons[1].pin); + + if (joystick[Direction::UP].pin != 0xFF) + detachInterrupt(joystick[Direction::UP].pin); + + if (joystick[Direction::DOWN].pin != 0xFF) + detachInterrupt(joystick[Direction::DOWN].pin); + + if (joystick[Direction::LEFT].pin != 0xFF) + detachInterrupt(joystick[Direction::LEFT].pin); + + if (joystick[Direction::RIGHT].pin != 0xFF) + detachInterrupt(joystick[Direction::RIGHT].pin); +} + +// Attempt to resolve a GPIO pin for the user button, honoring userPrefs.jsonc and device settings +// This helper method isn't used by the TwoButtonExtended class itself, it could be moved elsewhere. +// Intention is to pass this value to TwoButtonExtended::setWiring in the setupNicheGraphics method. +uint8_t TwoButtonExtended::getUserButtonPin() +{ + uint8_t pin = 0xFF; // Unset + + // Use default pin for variant, if no better source +#ifdef BUTTON_PIN + pin = BUTTON_PIN; +#endif + + // From userPrefs.jsonc, if set +#ifdef USERPREFS_BUTTON_PIN + pin = USERPREFS_BUTTON_PIN; +#endif + + // From user's override in device settings, if set + if (config.device.button_gpio) + pin = config.device.button_gpio; + + return pin; +} + +// Configures the wiring and logic of either button +// Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp +void TwoButtonExtended::setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup) +{ + // Prevent the same GPIO being assigned to multiple buttons + // Allows an edge case when the user remaps hardware buttons using device settings, due to a broken user button + for (uint8_t i = 0; i < whichButton; i++) { + if (buttons[i].pin == pin) { + LOG_WARN("Attempted reuse of GPIO %d. Ignoring assignment whichButton=%d", pin, whichButton); + return; + } + } + + assert(whichButton < 2); + buttons[whichButton].pin = pin; + buttons[whichButton].activeLogic = LOW; + + pinMode(buttons[whichButton].pin, internalPullup ? INPUT_PULLUP : INPUT); +} + +// Configures the wiring and logic of the joystick buttons +// Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp +void TwoButtonExtended::setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup) +{ + if (joystick[Direction::UP].pin == uPin || joystick[Direction::DOWN].pin == dPin || joystick[Direction::LEFT].pin == lPin || + joystick[Direction::RIGHT].pin == rPin) { + LOG_WARN("Attempted reuse of Joystick GPIO. Ignoring assignment"); + return; + } + + joystick[Direction::UP].pin = uPin; + joystick[Direction::DOWN].pin = dPin; + joystick[Direction::LEFT].pin = lPin; + joystick[Direction::RIGHT].pin = rPin; + joystickActiveLogic = LOW; + + pinMode(joystick[Direction::UP].pin, internalPullup ? INPUT_PULLUP : INPUT); + pinMode(joystick[Direction::DOWN].pin, internalPullup ? INPUT_PULLUP : INPUT); + pinMode(joystick[Direction::LEFT].pin, internalPullup ? INPUT_PULLUP : INPUT); + pinMode(joystick[Direction::RIGHT].pin, internalPullup ? INPUT_PULLUP : INPUT); +} + +void TwoButtonExtended::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs) +{ + assert(whichButton < 2); + buttons[whichButton].debounceLength = debounceMs; + buttons[whichButton].longpressLength = longpressMs; +} + +void TwoButtonExtended::setJoystickDebounce(uint32_t debounceMs) +{ + joystickDebounceLength = debounceMs; +} + +// Set what should happen when a button becomes pressed +// Use this to implement a "while held" behavior +void TwoButtonExtended::setHandlerDown(uint8_t whichButton, Callback onDown) +{ + assert(whichButton < 2); + buttons[whichButton].onDown = onDown; +} + +// Set what should happen when a button becomes unpressed +// Use this to implement a "While held" behavior +void TwoButtonExtended::setHandlerUp(uint8_t whichButton, Callback onUp) +{ + assert(whichButton < 2); + buttons[whichButton].onUp = onUp; +} + +// Set what should happen when a "short press" event has occurred +void TwoButtonExtended::setHandlerShortPress(uint8_t whichButton, Callback onPress) +{ + assert(whichButton < 2); + buttons[whichButton].onPress = onPress; +} + +// Set what should happen when a "long press" event has fired +// Note: this will occur while the button is still held +void TwoButtonExtended::setHandlerLongPress(uint8_t whichButton, Callback onLongPress) +{ + assert(whichButton < 2); + buttons[whichButton].onLongPress = onLongPress; +} + +// Set what should happen when a joystick button becomes pressed +// Use this to implement a "while held" behavior +void TwoButtonExtended::setJoystickDownHandlers(Callback uDown, Callback dDown, Callback lDown, Callback rDown) +{ + joystick[Direction::UP].onDown = uDown; + joystick[Direction::DOWN].onDown = dDown; + joystick[Direction::LEFT].onDown = lDown; + joystick[Direction::RIGHT].onDown = rDown; +} + +// Set what should happen when a joystick button becomes unpressed +// Use this to implement a "while held" behavior +void TwoButtonExtended::setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp) +{ + joystick[Direction::UP].onUp = uUp; + joystick[Direction::DOWN].onUp = dUp; + joystick[Direction::LEFT].onUp = lUp; + joystick[Direction::RIGHT].onUp = rUp; +} + +// Set what should happen when a "press" event has fired +// Note: this will occur while the joystick button is still held +void TwoButtonExtended::setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress) +{ + joystick[Direction::UP].onPress = uPress; + joystick[Direction::DOWN].onPress = dPress; + joystick[Direction::LEFT].onPress = lPress; + joystick[Direction::RIGHT].onPress = rPress; +} + +// Handle the start of a press to the primary button +// Wakes our button thread +void TwoButtonExtended::isrPrimary() +{ + static volatile bool isrRunning = false; + + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->buttons[0].state == State::REST) { + b->buttons[0].state = State::IRQ; + b->buttons[0].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; + } +} + +// Handle the start of a press to the secondary button +// Wakes our button thread +void TwoButtonExtended::isrSecondary() +{ + static volatile bool isrRunning = false; + + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->buttons[1].state == State::REST) { + b->buttons[1].state = State::IRQ; + b->buttons[1].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; + } +} + +// Handle the start of a press to the joystick buttons +// Also wakes our button thread +void TwoButtonExtended::isrJoystickUp() +{ + static volatile bool isrRunning = false; + + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->joystick[Direction::UP].state == State::REST) { + b->joystick[Direction::UP].state = State::IRQ; + b->joystick[Direction::UP].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; + } +} + +void TwoButtonExtended::isrJoystickDown() +{ + static volatile bool isrRunning = false; + + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->joystick[Direction::DOWN].state == State::REST) { + b->joystick[Direction::DOWN].state = State::IRQ; + b->joystick[Direction::DOWN].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; + } +} + +void TwoButtonExtended::isrJoystickLeft() +{ + static volatile bool isrRunning = false; + + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->joystick[Direction::LEFT].state == State::REST) { + b->joystick[Direction::LEFT].state = State::IRQ; + b->joystick[Direction::LEFT].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; + } +} + +void TwoButtonExtended::isrJoystickRight() +{ + static volatile bool isrRunning = false; + + if (!isrRunning) { + isrRunning = true; + TwoButtonExtended *b = TwoButtonExtended::getInstance(); + if (b->joystick[Direction::RIGHT].state == State::REST) { + b->joystick[Direction::RIGHT].state = State::IRQ; + b->joystick[Direction::RIGHT].irqAtMillis = millis(); + b->startThread(); + } + isrRunning = false; + } +} + +// Concise method to start our button thread +// Follows an ISR, listening for button release +void TwoButtonExtended::startThread() +{ + if (!OSThread::enabled) { + OSThread::setInterval(10); + OSThread::enabled = true; + } +} + +// Concise method to stop our button thread +// Called when we no longer need to poll for button release +void TwoButtonExtended::stopThread() +{ + if (OSThread::enabled) { + OSThread::disable(); + } + + // Reset both buttons manually + // Just in case an IRQ fires during the process of resetting the system + // Can occur with super rapid presses? + buttons[0].state = REST; + buttons[1].state = REST; + joystick[Direction::UP].state = REST; + joystick[Direction::DOWN].state = REST; + joystick[Direction::LEFT].state = REST; + joystick[Direction::RIGHT].state = REST; +} + +// Our button thread +// Started by an IRQ, on either button +// Polls for button releases +// Stops when both buttons released +int32_t TwoButtonExtended::runOnce() +{ + constexpr uint8_t BUTTON_COUNT = sizeof(buttons) / sizeof(Button); + constexpr uint8_t JOYSTICK_COUNT = sizeof(joystick) / sizeof(SimpleButton); + + // Allow either button to request that our thread should continue polling + bool awaitingRelease = false; + + // Check both primary and secondary buttons + for (uint8_t i = 0; i < BUTTON_COUNT; i++) { + switch (buttons[i].state) { + // No action: button has not been pressed + case REST: + break; + + // New press detected by interrupt + case IRQ: + powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer) + buttons[i].onDown(); // Run callback: press has begun (possible hold behavior) + buttons[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled + awaitingRelease = true; // Mark that polling-for-release should continue + break; + + // An existing press continues + // Not held long enough to register as longpress + case POLLING_UNFIRED: { + uint32_t length = millis() - buttons[i].irqAtMillis; + + // If button released since last thread tick, + if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { + buttons[i].onUp(); // Run callback: press has ended (possible release of a hold) + buttons[i].state = State::REST; // Mark that the button has reset + if (length > buttons[i].debounceLength && length < buttons[i].longpressLength) // If too short for longpress, + buttons[i].onPress(); // Run callback: press + } + // If button not yet released + else { + awaitingRelease = true; // Mark that polling-for-release should continue + if (length >= buttons[i].longpressLength) { + // Run callback: long press (once) + // Then continue waiting for release, to rearm + buttons[i].state = State::POLLING_FIRED; + buttons[i].onLongPress(); + } + } + break; + } + + // Button still held, but duration long enough that longpress event already fired + // Just waiting for release + case POLLING_FIRED: + // Release detected + if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) { + buttons[i].state = State::REST; + buttons[i].onUp(); // Callback: release of hold (in this case: *after* longpress has fired) + } + // Not yet released, keep polling + else + awaitingRelease = true; + break; + } + } + + // Check all the joystick directions + for (uint8_t i = 0; i < JOYSTICK_COUNT; i++) { + switch (joystick[i].state) { + // No action: button has not been pressed + case REST: + break; + + // New press detected by interrupt + case IRQ: + powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer) + joystick[i].onDown(); // Run callback: press has begun (possible hold behavior) + joystick[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled + awaitingRelease = true; // Mark that polling-for-release should continue + break; + + // An existing press continues + // Not held long enough to register as press + case POLLING_UNFIRED: { + uint32_t length = millis() - joystick[i].irqAtMillis; + + // If button released since last thread tick, + if (digitalRead(joystick[i].pin) != joystickActiveLogic) { + joystick[i].onUp(); // Run callback: press has ended (possible release of a hold) + joystick[i].state = State::REST; // Mark that the button has reset + } + // If button not yet released + else { + awaitingRelease = true; // Mark that polling-for-release should continue + if (length >= joystickDebounceLength) { + // Run callback: long press (once) + // Then continue waiting for release, to rearm + joystick[i].state = State::POLLING_FIRED; + joystick[i].onPress(); + } + } + break; + } + + // Button still held after press + // Just waiting for release + case POLLING_FIRED: + // Release detected + if (digitalRead(joystick[i].pin) != joystickActiveLogic) { + joystick[i].state = State::REST; + joystick[i].onUp(); // Callback: release of hold + } + // Not yet released, keep polling + else + awaitingRelease = true; + break; + } + } + + // If all buttons are now released + // we don't need to waste cpu resources polling + // IRQ will restart this thread when we next need it + if (!awaitingRelease) + stopThread(); + + // Run this method again, or don't.. + // Use whatever behavior was previously set by stopThread() or startThread() + return OSThread::interval; +} + +#ifdef ARCH_ESP32 + +// Detach our class' interrupts before lightsleep +// Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press +int TwoButtonExtended::beforeLightSleep(void *unused) +{ + stop(); + return 0; // Indicates success +} + +// Reconfigure our interrupts +// Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep +int TwoButtonExtended::afterLightSleep(esp_sleep_wakeup_cause_t cause) +{ + start(); + + // Manually trigger the button-down ISR + // - during light sleep, our ISR is disabled + // - if light sleep ends by button press, pretend our own ISR caught it + // - need to manually confirm by reading pin ourselves, to avoid occasional false positives + // (false positive only when using internal pullup resistors?) + if (cause == ESP_SLEEP_WAKEUP_GPIO && digitalRead(buttons[0].pin) == buttons[0].activeLogic) + isrPrimary(); + + return 0; // Indicates success +} + +#endif + +#endif diff --git a/src/graphics/niche/Inputs/TwoButtonExtended.h b/src/graphics/niche/Inputs/TwoButtonExtended.h new file mode 100644 index 0000000000..23fd78a2ac --- /dev/null +++ b/src/graphics/niche/Inputs/TwoButtonExtended.h @@ -0,0 +1,136 @@ +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +/* + +Re-usable NicheGraphics input source + +Short and Long press for up to two buttons +Interrupt driven + +*/ + +/* + +This expansion adds support for four more buttons +These buttons are single-action only, no long press +Interrupt driven + +*/ + +#pragma once + +#include "configuration.h" + +#include "assert.h" +#include "functional" + +#ifdef ARCH_ESP32 +#include "esp_sleep.h" // For light-sleep handling +#endif + +#include "Observer.h" + +namespace NicheGraphics::Inputs +{ + +class TwoButtonExtended : protected concurrency::OSThread +{ + public: + typedef std::function Callback; + + static uint8_t getUserButtonPin(); // Resolve the GPIO, considering the various possible source of definition + + static TwoButtonExtended *getInstance(); // Create or get the singleton instance + void start(); // Start handling button input + void stop(); // Stop handling button input (disconnect ISRs for sleep) + void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false); + void setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup = false); + void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs); + void setJoystickDebounce(uint32_t debounceMs); + void setHandlerDown(uint8_t whichButton, Callback onDown); + void setHandlerUp(uint8_t whichButton, Callback onUp); + void setHandlerShortPress(uint8_t whichButton, Callback onShortPress); + void setHandlerLongPress(uint8_t whichButton, Callback onLongPress); + void setJoystickDownHandlers(Callback uDown, Callback dDown, Callback ldown, Callback rDown); + void setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp); + void setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress); + + // Disconnect and reconnect interrupts for light sleep +#ifdef ARCH_ESP32 + int beforeLightSleep(void *unused); + int afterLightSleep(esp_sleep_wakeup_cause_t cause); +#endif + + private: + // Internal state of a specific button + enum State { + REST, // Up, no activity + IRQ, // Down detected, not yet handled + POLLING_UNFIRED, // Down handled, polling for release + POLLING_FIRED, // Longpress fired, button still held + }; + + // Joystick Directions + enum Direction { UP = 0, DOWN, LEFT, RIGHT }; + + // Data used for direction (single-action) buttons + class SimpleButton + { + public: + // Per-button config + uint8_t pin = 0xFF; // 0xFF: unset + volatile State state = State::REST; // Internal state + volatile uint32_t irqAtMillis; // millis() when button went down + + // Per-button event callbacks + static void noop(){}; + std::function onDown = noop; + std::function onUp = noop; + std::function onPress = noop; + }; + + // Data used for double-action buttons + class Button : public SimpleButton + { + public: + // Per-button extended config + bool activeLogic = LOW; // Active LOW by default. + uint32_t debounceLength = 50; // Minimum length for shortpress in ms + uint32_t longpressLength = 500; // Time until longpress in ms + + // Per-button event callbacks + std::function onLongPress = noop; + }; + +#ifdef ARCH_ESP32 + // Get notified when lightsleep begins and ends + CallbackObserver lsObserver = + CallbackObserver(this, &TwoButtonExtended::beforeLightSleep); + CallbackObserver lsEndObserver = + CallbackObserver(this, &TwoButtonExtended::afterLightSleep); +#endif + + int32_t runOnce() override; // Timer method. Polls for button release + + void startThread(); // Start polling for release + void stopThread(); // Stop polling for release + + static void isrPrimary(); // User Button ISR + static void isrSecondary(); // optional aux button or joystick center + static void isrJoystickUp(); + static void isrJoystickDown(); + static void isrJoystickLeft(); + static void isrJoystickRight(); + + TwoButtonExtended(); // Constructor made private: force use of Button::instance() + + // Info about both buttons + Button buttons[2]; + bool joystickActiveLogic = LOW; // Active LOW by default + uint32_t joystickDebounceLength = 50; // time until press in ms + SimpleButton joystick[4]; +}; + +}; // namespace NicheGraphics::Inputs + +#endif diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h index 7fb8903035..cdda93cdfa 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h @@ -19,7 +19,7 @@ // Shared NicheGraphics components // -------------------------------- #include "graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h" -#include "graphics/niche/Inputs/TwoButton.h" +#include "graphics/niche/Inputs/TwoButtonExtended.h" void setupNicheGraphics() { @@ -54,7 +54,12 @@ void setupNicheGraphics() InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Customize default settings - inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise + inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise +#if HAS_TRACKBALL + inkhud->persistence->settings.joystick.enabled = true; // Device uses a joystick + inkhud->persistence->settings.joystick.alignment = 3; // 270 degrees + inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Use joystick instead +#endif inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side @@ -75,16 +80,30 @@ void setupNicheGraphics() // Buttons // -------------------------- - Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + Inputs::TwoButtonExtended *buttons = Inputs::TwoButtonExtended::getInstance(); // Shared NicheGraphics component - // #0: Main User Button - buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); + // #0: User Button + buttons->setWiring(0, Inputs::TwoButtonExtended::getUserButtonPin()); buttons->setTiming(0, 75, 500); buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); +#if HAS_TRACKBALL + // #1: Joystick Center + buttons->setWiring(1, TB_PRESS); + buttons->setTiming(1, 75, 500); + buttons->setHandlerShortPress(1, [inkhud]() { inkhud->stickCenterShort(); }); + buttons->setHandlerLongPress(1, [inkhud]() { inkhud->stickCenterLong(); }); + + // Joystick Directions + buttons->setJoystickWiring(TB_UP, TB_DOWN, TB_LEFT, TB_RIGHT); + buttons->setJoystickDebounce(50); + buttons->setJoystickPressHandlers([inkhud]() { inkhud->stickUp(); }, [inkhud]() { inkhud->stickDown(); }, + [inkhud]() { inkhud->stickLeft(); }, [inkhud]() { inkhud->stickRight(); }); +#endif + // Begin handling button events buttons->start(); } -#endif \ No newline at end of file +#endif