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