From 67fc0c57bf4c0a2c4c94a305480bc1ba4ab8a878 Mon Sep 17 00:00:00 2001 From: scobert Date: Sun, 16 Nov 2025 16:29:20 -0800 Subject: [PATCH 01/12] TwoButtonExtened mirrors TwoButton but added joystick functionality --- .../niche/Inputs/TwoButtonExtended.cpp | 523 ++++++++++++++++++ src/graphics/niche/Inputs/TwoButtonExtended.h | 138 +++++ .../seeed_wio_tracker_L1_eink/nicheGraphics.h | 12 +- 3 files changed, 667 insertions(+), 6 deletions(-) create mode 100644 src/graphics/niche/Inputs/TwoButtonExtended.cpp create mode 100644 src/graphics/niche/Inputs/TwoButtonExtended.h diff --git a/src/graphics/niche/Inputs/TwoButtonExtended.cpp b/src/graphics/niche/Inputs/TwoButtonExtended.cpp new file mode 100644 index 0000000000..b6c97098b8 --- /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 TweButton 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..eae5cc1b2a --- /dev/null +++ b/src/graphics/niche/Inputs/TwoButtonExtended.h @@ -0,0 +1,138 @@ +#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..f305cd96bb 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() { @@ -68,23 +68,23 @@ void setupNicheGraphics() inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 - + // Start running InkHUD inkhud->begin(); // 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()); + buttons->setWiring(0, Inputs::TwoButtonExtended::getUserButtonPin()); buttons->setTiming(0, 75, 500); buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); - // Begin handling button events + // Begin handling button events buttons->start(); } -#endif \ No newline at end of file +#endif From 4f5ef6c6f71f30b1734ea7a93e5caefbceb02994 Mon Sep 17 00:00:00 2001 From: zeropt Date: Sun, 16 Nov 2025 18:49:10 -0800 Subject: [PATCH 02/12] basic ui navigation with a joystick settings->joystick.enabled setting added and SETTINGS_VERSION incremented by one in InkHUD/Persistence.h in seeed_wio_tracker_L1_eink/nicheGraphics.h enable joystick and disable "Next Tile" menu item in implement prevTile and prevApplet functions in InkHUD/WindowManager.h,cpp and InkHUD/InkHUD.h,cpp onStickCenterShort, onStickCenterLong, onStickUp, onStickDown, onStickLeft, and onStickRight functions added to: - InkHUD/InkHUD.h,cpp - InkHUD/Events.h,cpp - InkHUD/Applet.h change navigation actions in InkHUD/Events.cpp events based on whether the joystick is enabled or not in seeed_wio_tracker_L1_eink/nicheGraphics.h connect joystick events to the new joystick handler functions --- src/graphics/niche/InkHUD/Applet.h | 6 + src/graphics/niche/InkHUD/Events.cpp | 164 +++++++++++++++++- src/graphics/niche/InkHUD/Events.h | 6 + src/graphics/niche/InkHUD/InkHUD.cpp | 51 ++++++ src/graphics/niche/InkHUD/InkHUD.h | 9 + src/graphics/niche/InkHUD/Persistence.h | 8 +- src/graphics/niche/InkHUD/WindowManager.cpp | 85 +++++++++ src/graphics/niche/InkHUD/WindowManager.h | 2 + .../seeed_wio_tracker_L1_eink/nicheGraphics.h | 22 ++- 9 files changed, 347 insertions(+), 6 deletions(-) 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/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..2a6f952af2 100644 --- a/src/graphics/niche/InkHUD/InkHUD.cpp +++ b/src/graphics/niche/InkHUD/InkHUD.cpp @@ -80,6 +80,42 @@ 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() +{ + events->onStickUp(); +} + +// Call this when your joystick gets a down input +void InkHUD::InkHUD::stickDown() +{ + events->onStickDown(); +} + +// Call this when your joystick gets a left input +void InkHUD::InkHUD::stickLeft() +{ + events->onStickLeft(); +} + +// Call this when your joystick gets a right input +void InkHUD::InkHUD::stickRight() +{ + events->onStickRight(); +} + // 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 +124,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() @@ -102,6 +146,13 @@ 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() { diff --git a/src/graphics/niche/InkHUD/InkHUD.h b/src/graphics/niche/InkHUD/InkHUD.h index 13839ea22c..3e1a59f4b6 100644 --- a/src/graphics/niche/InkHUD/InkHUD.h +++ b/src/graphics/niche/InkHUD/InkHUD.h @@ -56,13 +56,22 @@ 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 nextTile(); + void prevTile(); void rotate(); void toggleBatteryIcon(); diff --git a/src/graphics/niche/InkHUD/Persistence.h b/src/graphics/niche/InkHUD/Persistence.h index b85274c87d..9ed6bd2860 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,12 @@ class Persistence bool safeShutdownSeen = false; } tips; + // Joystick settings + struct Joystick { + // Modifies the UI for joystick use + bool enabled = false; + } 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..c6b39d8473 100644 --- a/src/graphics/niche/InkHUD/WindowManager.cpp +++ b/src/graphics/niche/InkHUD/WindowManager.cpp @@ -98,6 +98,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() @@ -155,6 +187,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() { diff --git a/src/graphics/niche/InkHUD/WindowManager.h b/src/graphics/niche/InkHUD/WindowManager.h index 4d1aedf1be..1332675818 100644 --- a/src/graphics/niche/InkHUD/WindowManager.h +++ b/src/graphics/niche/InkHUD/WindowManager.h @@ -28,8 +28,10 @@ class WindowManager // - call these to make stuff change void nextTile(); + void prevTile(); void openMenu(); void nextApplet(); + void prevApplet(); void rotate(); void toggleBatteryIcon(); diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h index f305cd96bb..398dd8865c 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h @@ -55,6 +55,10 @@ void setupNicheGraphics() // Customize default settings inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise +#if HAS_TRACKBALL + inkhud->persistence->settings.joystick.enabled = true; // Device uses a joystick + 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 @@ -77,13 +81,27 @@ void setupNicheGraphics() Inputs::TwoButtonExtended *buttons = Inputs::TwoButtonExtended::getInstance(); // Shared NicheGraphics component - // #0: Main User Button + // #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(); }); - // Begin handling button events +#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(30); + buttons->setJoystickPressHandlers([inkhud]() { inkhud->stickUp(); }, [inkhud]() { inkhud->stickDown(); }, + [inkhud]() { inkhud->stickLeft(); }, [inkhud]() { inkhud->stickRight(); }); +#endif + + // Begin handling button events buttons->start(); } From a4ee7ba9ecc1e614087270f65376079060a7c6c0 Mon Sep 17 00:00:00 2001 From: zeropt Date: Sun, 16 Nov 2025 19:17:49 -0800 Subject: [PATCH 03/12] handle joystick input in NotificationApplet and TipsApplet Both the joystick center short press and the user button short press can be used to advance through the Tips applet. dismiss notifications with any joystick input --- .../Notification/NotificationApplet.cpp | 36 +++++++++++++++++++ .../System/Notification/NotificationApplet.h | 6 ++++ .../InkHUD/Applets/System/Tips/TipsApplet.cpp | 6 ++++ .../InkHUD/Applets/System/Tips/TipsApplet.h | 1 + 4 files changed, 49 insertions(+) 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..8f85f8fb6e 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp @@ -232,4 +232,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 From 1223bf0edb80db6b2e95b8c6baf4dce0e02b4a22 Mon Sep 17 00:00:00 2001 From: scobert Date: Sun, 16 Nov 2025 20:06:52 -0800 Subject: [PATCH 04/12] MenuApplet controls allows menu navigation including a back button --- .../InkHUD/Applets/System/Menu/MenuApplet.cpp | 120 ++++++++++++++++-- .../InkHUD/Applets/System/Menu/MenuApplet.h | 9 +- 2 files changed, 115 insertions(+), 14 deletions(-) diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp index 09f76ed461..7afc50b148 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp @@ -287,14 +287,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: @@ -332,20 +335,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,30 +483,117 @@ 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); - // Move menu cursor to next entry, then update if (cursorShown) - cursor = (cursor + 1) % items.size(); + execute(items.at(cursor)); else - cursorShown = true; + 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::onButtonLongPress() +void InkHUD::MenuApplet::onStickUp() { - // Push the auto-close timer back OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - if (cursorShown) - execute(items.at(cursor)); + // Move menu cursor to previous entry, then update + if (cursor == 0) + cursor = items.size() - 1; else - showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close + cursor--; + + if (!cursorShown) + cursorShown = true; - // 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 + requestUpdate(Drivers::EInk::UpdateTypes::FAST); +} + +void InkHUD::MenuApplet::onStickDown() +{ + 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); +} + +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() +{ + OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); + + if (cursorShown) + execute(items.at(cursor)); if (!wantsToRender()) requestUpdate(Drivers::EInk::UpdateTypes::FAST); } @@ -796,4 +890,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 From 9ae43d5c3af60fba68bc9fe6e24fc0f6496e2802 Mon Sep 17 00:00:00 2001 From: zeropt Date: Sun, 16 Nov 2025 20:36:25 -0800 Subject: [PATCH 05/12] add AlignStickApplet for aligning the joystick with the screen add joystick.aligned and joystick.alignment to InkHUD/Persistence.h for storing alignment status and relative angle create AlignStick applet that prompts the user for a joystick input and rotates the controls to align with the screen AlignStick applet is run after the tips applet if the joystick is enabled and not aligned add menu item for opening the AlignStick applet --- .../System/AlignStick/AlignStickApplet.cpp | 203 ++++++++++++++++++ .../System/AlignStick/AlignStickApplet.h | 51 +++++ .../InkHUD/Applets/System/Menu/MenuAction.h | 1 + .../InkHUD/Applets/System/Menu/MenuApplet.cpp | 6 + src/graphics/niche/InkHUD/InkHUD.cpp | 73 ++++++- src/graphics/niche/InkHUD/InkHUD.h | 2 + src/graphics/niche/InkHUD/Persistence.h | 9 +- src/graphics/niche/InkHUD/WindowManager.cpp | 14 ++ src/graphics/niche/InkHUD/WindowManager.h | 1 + .../seeed_wio_tracker_L1_eink/nicheGraphics.h | 1 + 10 files changed, 356 insertions(+), 5 deletions(-) create mode 100644 src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp create mode 100644 src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h 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..1435ae527e --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp @@ -0,0 +1,203 @@ +#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 + + // Draw center circle + fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width / 5), BLACK); + fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width / 5) - stroke, WHITE); + + // Draw filled up arrow + drawDirection(centerX, centerY - width / 2, Direction::UP, width * 0.4, chamfer, BLACK); + + // Draw down arrow + drawDirection(centerX, centerY + width / 2, Direction::DOWN, width * 0.4, chamfer, BLACK); + drawDirection(centerX, centerY + width / 2 - stroke, Direction::DOWN, width * 0.4 - stroke * 3, 0, WHITE); + + // Draw left arrow + drawDirection(centerX - width / 2, centerY, Direction::LEFT, width * 0.4, chamfer, BLACK); + drawDirection(centerX - width / 2 + stroke, centerY, Direction::LEFT, width * 0.4 - stroke * 3, 0, WHITE); + + // Draw right arrow + drawDirection(centerX + width / 2, centerY, Direction::RIGHT, width * 0.4, chamfer, BLACK); + drawDirection(centerX + width / 2 - stroke, centerY, Direction::RIGHT, width * 0.4 - stroke * 3, 0, WHITE); + } +} + +// Draw a scalable joystick direction arrow +// a right-triangle with blunted tips +/* + _ <--point + / \ + / \ + / \ + |_______| + + | width | +*/ +void InkHUD::AlignStickApplet::drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t width, + uint16_t chamfer, Color color) +{ + uint16_t chamferW = chamfer * 2 + 1; + uint16_t triangleW = (width + 1) / 2 - chamfer; + + // 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 - chamferW - triangleW + 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 - chamferW - triangleW + 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..060f5a90a3 --- /dev/null +++ b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h @@ -0,0 +1,51 @@ +#ifdef MESHTASTIC_INCLUDE_INKHUD + +/* + +System Applet for manually aligning the joystick with the screen + +should be run on 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 width, 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 7afc50b148..058243390a 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++; @@ -324,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, diff --git a/src/graphics/niche/InkHUD/InkHUD.cpp b/src/graphics/niche/InkHUD/InkHUD.cpp index 2a6f952af2..1a341b2b3f 100644 --- a/src/graphics/niche/InkHUD/InkHUD.cpp +++ b/src/graphics/niche/InkHUD/InkHUD.cpp @@ -95,25 +95,77 @@ void InkHUD::InkHUD::stickCenterLong() // Call this when your joystick gets an up input void InkHUD::InkHUD::stickUp() { - events->onStickUp(); + 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() { - events->onStickDown(); + 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() { - events->onStickLeft(); + 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() { - events->onStickRight(); + 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 @@ -139,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() @@ -159,6 +217,13 @@ 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 3e1a59f4b6..da23275044 100644 --- a/src/graphics/niche/InkHUD/InkHUD.h +++ b/src/graphics/niche/InkHUD/InkHUD.h @@ -70,9 +70,11 @@ class InkHUD 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 9ed6bd2860..5054b7234f 100644 --- a/src/graphics/niche/InkHUD/Persistence.h +++ b/src/graphics/niche/InkHUD/Persistence.h @@ -96,10 +96,17 @@ class Persistence bool safeShutdownSeen = false; } tips; - // Joystick settings + // 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 diff --git a/src/graphics/niche/InkHUD/WindowManager.cpp b/src/graphics/niche/InkHUD/WindowManager.cpp index c6b39d8473..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" @@ -138,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() @@ -423,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); @@ -445,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 1332675818..5def48f8c8 100644 --- a/src/graphics/niche/InkHUD/WindowManager.h +++ b/src/graphics/niche/InkHUD/WindowManager.h @@ -30,6 +30,7 @@ class WindowManager void nextTile(); void prevTile(); void openMenu(); + void openAlignStick(); void nextApplet(); void prevApplet(); void rotate(); diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h index 398dd8865c..ad5dc8010f 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h @@ -57,6 +57,7 @@ void setupNicheGraphics() 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 From 3a158c8de10e4fd9ad3565e6e7fa41e4bf65739d Mon Sep 17 00:00:00 2001 From: zeropt Date: Sun, 16 Nov 2025 20:37:25 -0800 Subject: [PATCH 06/12] update tips applet with joystick controls --- .../InkHUD/Applets/System/Tips/TipsApplet.cpp | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp index 8f85f8fb6e..ce11f20894 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: select / switch tile"); + 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); From 658d3cfb72cbda3f968ec8aeb05e9aa79259c4d4 Mon Sep 17 00:00:00 2001 From: zeropt Date: Sun, 16 Nov 2025 20:42:24 -0800 Subject: [PATCH 07/12] format InkHUD additions --- .../System/AlignStick/AlignStickApplet.h | 3 +- .../InkHUD/Applets/System/Menu/MenuApplet.cpp | 69 +++++++++---------- src/graphics/niche/Inputs/TwoButtonExtended.h | 50 +++++++------- .../seeed_wio_tracker_L1_eink/nicheGraphics.h | 10 +-- 4 files changed, 63 insertions(+), 69 deletions(-) diff --git a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h index 060f5a90a3..b53c1cadbf 100644 --- a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h @@ -42,8 +42,7 @@ class AlignStickApplet : public SystemApplet }; void drawStick(uint16_t centerX, uint16_t centerY, uint16_t width); - void drawDirection(uint16_t pointX, uint16_t pointY, - Direction direction, uint16_t width, uint16_t chamfer, Color color); + void drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t width, uint16_t chamfer, Color color); }; } // namespace NicheGraphics::InkHUD diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp index 058243390a..73e757acb4 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp @@ -291,17 +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; + previousPage = MenuPage::EXIT; break; case SEND: populateSendPage(); - previousPage = MenuPage::ROOT; + previousPage = MenuPage::ROOT; break; case CANNEDMESSAGE_RECIPIENT: populateRecipientPage(); - previousPage = MenuPage::OPTIONS; + previousPage = MenuPage::OPTIONS; break; case OPTIONS: @@ -341,24 +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; + previousPage = MenuPage::ROOT; break; case APPLETS: populateAppletPage(); items.push_back(MenuItem("Exit", MenuPage::EXIT)); - previousPage = MenuPage::OPTIONS; + previousPage = MenuPage::OPTIONS; break; case AUTOSHOW: populateAutoshowPage(); items.push_back(MenuItem("Exit", MenuPage::EXIT)); - previousPage = MenuPage::OPTIONS; + previousPage = MenuPage::OPTIONS; break; case RECENTS: populateRecentsPage(); - previousPage = MenuPage::OPTIONS; + previousPage = MenuPage::OPTIONS; break; case EXIT: @@ -493,29 +493,29 @@ void InkHUD::MenuApplet::onButtonShortPress() // Push the auto-close timer back OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - // Move menu cursor to next entry, then update - if (cursorShown) + // Move menu cursor to next entry, then update + if (cursorShown) cursor = (cursor + 1) % items.size(); - else + else cursorShown = true; requestUpdate(Drivers::EInk::UpdateTypes::FAST); } else { - // Exit the menu - showPage(MenuPage::EXIT); - - requestUpdate(Drivers::EInk::UpdateTypes::FAST); + // 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 + // Push the auto-close timer back OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); - if (cursorShown) + if (cursorShown) execute(items.at(cursor)); - else + 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, @@ -524,36 +524,33 @@ void InkHUD::MenuApplet::onButtonLongPress() if (!wantsToRender()) requestUpdate(Drivers::EInk::UpdateTypes::FAST); } else { - // Exit the menu - showPage(MenuPage::EXIT); + // Exit the menu + showPage(MenuPage::EXIT); - requestUpdate(Drivers::EInk::UpdateTypes::FAST); + 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() +void InkHUD::MenuApplet::onStickCenterShort() { OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); if (cursorShown) - execute(items.at(cursor)); + execute(items.at(cursor)); else - showPage(MenuPage::EXIT); + showPage(MenuPage::EXIT); if (!wantsToRender()) - requestUpdate(Drivers::EInk::UpdateTypes::FAST); - + requestUpdate(Drivers::EInk::UpdateTypes::FAST); } -void InkHUD::MenuApplet::onStickCenterLong() +void InkHUD::MenuApplet::onStickCenterLong() { // Exit the menu showPage(MenuPage::EXIT); requestUpdate(Drivers::EInk::UpdateTypes::FAST); - } void InkHUD::MenuApplet::onStickUp() @@ -562,12 +559,12 @@ void InkHUD::MenuApplet::onStickUp() // Move menu cursor to previous entry, then update if (cursor == 0) - cursor = items.size() - 1; + cursor = items.size() - 1; else - cursor--; - + cursor--; + if (!cursorShown) - cursorShown = true; + cursorShown = true; requestUpdate(Drivers::EInk::UpdateTypes::FAST); } @@ -578,9 +575,9 @@ void InkHUD::MenuApplet::onStickDown() // Move menu cursor to next entry, then update if (cursorShown) - cursor = (cursor + 1) % items.size(); - else - cursorShown = true; + cursor = (cursor + 1) % items.size(); + else + cursorShown = true; requestUpdate(Drivers::EInk::UpdateTypes::FAST); } @@ -599,7 +596,7 @@ void InkHUD::MenuApplet::onStickRight() OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL); if (cursorShown) - execute(items.at(cursor)); + execute(items.at(cursor)); if (!wantsToRender()) requestUpdate(Drivers::EInk::UpdateTypes::FAST); } diff --git a/src/graphics/niche/Inputs/TwoButtonExtended.h b/src/graphics/niche/Inputs/TwoButtonExtended.h index eae5cc1b2a..23fd78a2ac 100644 --- a/src/graphics/niche/Inputs/TwoButtonExtended.h +++ b/src/graphics/niche/Inputs/TwoButtonExtended.h @@ -9,7 +9,7 @@ Interrupt driven */ -/* +/* This expansion adds support for four more buttons These buttons are single-action only, no long press @@ -17,7 +17,6 @@ Interrupt driven */ - #pragma once #include "configuration.h" @@ -42,12 +41,12 @@ class TwoButtonExtended : protected concurrency::OSThread 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 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 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); @@ -55,7 +54,6 @@ class TwoButtonExtended : protected concurrency::OSThread 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 @@ -78,27 +76,27 @@ class TwoButtonExtended : protected concurrency::OSThread // 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; + 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 + 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; @@ -106,8 +104,8 @@ class TwoButtonExtended : protected concurrency::OSThread #ifdef ARCH_ESP32 // Get notified when lightsleep begins and ends - CallbackObserver lsObserver = - CallbackObserver(this, &TwoButtonExtended::beforeLightSleep); + CallbackObserver lsObserver = + CallbackObserver(this, &TwoButtonExtended::beforeLightSleep); CallbackObserver lsEndObserver = CallbackObserver(this, &TwoButtonExtended::afterLightSleep); #endif @@ -123,13 +121,13 @@ class TwoButtonExtended : protected concurrency::OSThread 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 + bool joystickActiveLogic = LOW; // Active LOW by default + uint32_t joystickDebounceLength = 50; // time until press in ms SimpleButton joystick[4]; }; diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h index ad5dc8010f..b48b186363 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h @@ -54,11 +54,11 @@ 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 + 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 @@ -73,7 +73,7 @@ void setupNicheGraphics() inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 - + // Start running InkHUD inkhud->begin(); From 9afbb259cb16dab3a7c18a6d8bdc7e695b476805 Mon Sep 17 00:00:00 2001 From: zeropt Date: Tue, 18 Nov 2025 19:26:47 -0800 Subject: [PATCH 08/12] fix stroke consistency when resizing joystick graphic --- .../System/AlignStick/AlignStickApplet.cpp | 40 ++++++++++--------- .../System/AlignStick/AlignStickApplet.h | 2 +- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp index 1435ae527e..3b9a85dc1c 100644 --- a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp @@ -55,44 +55,46 @@ void InkHUD::AlignStickApplet::drawStick(uint16_t centerX, uint16_t centerY, uin } 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 / 5), BLACK); - fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width / 5) - stroke, WHITE); + 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, width * 0.4, chamfer, BLACK); + drawDirection(centerX, centerY - width / 2, Direction::UP, arrowW, chamfer, BLACK); // Draw down arrow - drawDirection(centerX, centerY + width / 2, Direction::DOWN, width * 0.4, chamfer, BLACK); - drawDirection(centerX, centerY + width / 2 - stroke, Direction::DOWN, width * 0.4 - stroke * 3, 0, WHITE); + 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, width * 0.4, chamfer, BLACK); - drawDirection(centerX - width / 2 + stroke, centerY, Direction::LEFT, width * 0.4 - stroke * 3, 0, WHITE); + 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, width * 0.4, chamfer, BLACK); - drawDirection(centerX + width / 2 - stroke, centerY, Direction::RIGHT, width * 0.4 - stroke * 3, 0, WHITE); + 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 - / \ - / \ - / \ - |_______| + _ <--point + ^ / \ + | / \ + size / \ + | / \ + v |_________| - | width | */ -void InkHUD::AlignStickApplet::drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t width, +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 = (width + 1) / 2 - chamfer; + uint16_t triangleW = size - chamferW; // Draw arrow switch (direction) { @@ -106,7 +108,7 @@ void InkHUD::AlignStickApplet::drawDirection(uint16_t pointX, uint16_t pointY, D break; case Direction::DOWN: fillRect(pointX - chamfer, pointY - triangleW + 1, chamferW, triangleW, color); - fillRect(pointX - chamfer - triangleW, pointY - chamferW - triangleW + 1, chamferW + triangleW * 2, chamferW, 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, @@ -122,7 +124,7 @@ void InkHUD::AlignStickApplet::drawDirection(uint16_t pointX, uint16_t pointY, D break; case Direction::RIGHT: fillRect(pointX - triangleW + 1, pointY - chamfer, triangleW, chamferW, color); - fillRect(pointX - chamferW - triangleW + 1, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, 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, diff --git a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h index b53c1cadbf..7e5add452a 100644 --- a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h @@ -42,7 +42,7 @@ class AlignStickApplet : public SystemApplet }; void drawStick(uint16_t centerX, uint16_t centerY, uint16_t width); - void drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t width, uint16_t chamfer, Color color); + void drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size, uint16_t chamfer, Color color); }; } // namespace NicheGraphics::InkHUD From 8f6d41fb121d54295eb41829811cc935716931cf Mon Sep 17 00:00:00 2001 From: zeropt Date: Tue, 18 Nov 2025 19:28:06 -0800 Subject: [PATCH 09/12] tweak button tips for order consistency --- src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp index ce11f20894..e158b5cc26 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Tips/TipsApplet.cpp @@ -125,7 +125,7 @@ void InkHUD::TipsApplet::onRender() cursorY += fontSmall.lineHeight() * 1.5; printAt(0, cursorY, "Joystick"); cursorY += fontSmall.lineHeight() * 1.2; - printAt(0, cursorY, "- short press: select / switch tile"); + printAt(0, cursorY, "- short press: switch tile / select"); cursorY += fontSmall.lineHeight() * 1.2; printAt(0, cursorY, "- long press: open / close menu"); } From e4bcf38f64ee688917ef18439affdf8818699140 Mon Sep 17 00:00:00 2001 From: zeropt Date: Tue, 18 Nov 2025 19:28:53 -0800 Subject: [PATCH 10/12] increase joystick debounce --- variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h index b48b186363..cdda93cdfa 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h @@ -97,7 +97,7 @@ void setupNicheGraphics() // Joystick Directions buttons->setJoystickWiring(TB_UP, TB_DOWN, TB_LEFT, TB_RIGHT); - buttons->setJoystickDebounce(30); + buttons->setJoystickDebounce(50); buttons->setJoystickPressHandlers([inkhud]() { inkhud->stickUp(); }, [inkhud]() { inkhud->stickDown(); }, [inkhud]() { inkhud->stickLeft(); }, [inkhud]() { inkhud->stickRight(); }); #endif From c48b80c8c107610743fd4980f8af2d7102e3fef6 Mon Sep 17 00:00:00 2001 From: zeropt Date: Wed, 19 Nov 2025 09:47:03 -0800 Subject: [PATCH 11/12] fix comments --- .../niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h | 2 +- src/graphics/niche/Inputs/TwoButtonExtended.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h index 7e5add452a..327025f343 100644 --- a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.h @@ -4,7 +4,7 @@ System Applet for manually aligning the joystick with the screen -should be run on at startup if the joystick is enabled +should be run at startup if the joystick is enabled and not aligned to the screen */ diff --git a/src/graphics/niche/Inputs/TwoButtonExtended.cpp b/src/graphics/niche/Inputs/TwoButtonExtended.cpp index b6c97098b8..287fb943fa 100644 --- a/src/graphics/niche/Inputs/TwoButtonExtended.cpp +++ b/src/graphics/niche/Inputs/TwoButtonExtended.cpp @@ -91,7 +91,7 @@ void TwoButtonExtended::stop() } // Attempt to resolve a GPIO pin for the user button, honoring userPrefs.jsonc and device settings -// This helper method isn't used by the TweButton class itself, it could be moved elsewhere. +// 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() { From 92ee34eacd2c6ed8890c29420c0f29821bd63c9d Mon Sep 17 00:00:00 2001 From: zeropt Date: Wed, 19 Nov 2025 09:53:51 -0800 Subject: [PATCH 12/12] remove unnecessary '+' --- .../niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp index 3b9a85dc1c..b73635e702 100644 --- a/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/AlignStick/AlignStickApplet.cpp @@ -56,7 +56,7 @@ void InkHUD::AlignStickApplet::drawStick(uint16_t centerX, uint16_t centerY, uin uint16_t chamfer = width < 80 ? 1 : 2; uint16_t stroke = 3; // pixels uint16_t arrowW = width * 0.22; - uint16_t hollowW = arrowW + -stroke * 2; + uint16_t hollowW = arrowW - stroke * 2; // Draw center circle fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2), BLACK);