Use InputManager from community-sdk

This commit is contained in:
Dave Allie 2025-12-06 12:35:41 +11:00
parent f0d92da8f2
commit 6414f85257
No known key found for this signature in database
GPG Key ID: F2FDDB3AD8D0276F
13 changed files with 134 additions and 163 deletions

@ -1 +1 @@
Subproject commit 90a19bb8a73d99d9ec57c8459af51838116557ac Subproject commit 8224d278c58e76abf781c2e015f28a09419f27b2

View File

@ -34,3 +34,4 @@ lib_deps =
zinggjm/GxEPD2@^1.6.5 zinggjm/GxEPD2@^1.6.5
https://github.com/leethomason/tinyxml2.git#11.0.0 https://github.com/leethomason/tinyxml2.git#11.0.0
BatteryMonitor=symlink://open-x4-sdk/libs/hardware/BatteryMonitor BatteryMonitor=symlink://open-x4-sdk/libs/hardware/BatteryMonitor
InputManager=symlink://open-x4-sdk/libs/hardware/InputManager

View File

@ -1,43 +0,0 @@
#include "Input.h"
#include <esp32-hal-adc.h>
void setupInputPinModes() {
pinMode(BTN_GPIO1, INPUT);
pinMode(BTN_GPIO2, INPUT);
pinMode(BTN_GPIO3, INPUT_PULLUP); // Power button
}
// Get currently pressed button by reading ADC values (and digital for power
// button)
Button getPressedButton() {
// Check BTN_GPIO3 (Power button) - digital read
if (digitalRead(BTN_GPIO3) == LOW) return POWER;
// Check BTN_GPIO1 (4 buttons on resistor ladder)
const int btn1 = analogRead(BTN_GPIO1);
if (btn1 < BTN_RIGHT_VAL + BTN_THRESHOLD) return RIGHT;
if (btn1 < BTN_LEFT_VAL + BTN_THRESHOLD) return LEFT;
if (btn1 < BTN_CONFIRM_VAL + BTN_THRESHOLD) return CONFIRM;
if (btn1 < BTN_BACK_VAL + BTN_THRESHOLD) return BACK;
// Check BTN_GPIO2 (2 buttons on resistor ladder)
const int btn2 = analogRead(BTN_GPIO2);
if (btn2 < BTN_VOLUME_DOWN_VAL + BTN_THRESHOLD) return VOLUME_DOWN;
if (btn2 < BTN_VOLUME_UP_VAL + BTN_THRESHOLD) return VOLUME_UP;
return NONE;
}
Input getInput(const long maxHoldMs) {
const Button button = getPressedButton();
if (button == NONE) return {NONE, 0};
const auto start = millis();
unsigned long held = 0;
while (getPressedButton() == button && (maxHoldMs < 0 || held < maxHoldMs)) {
delay(50);
held = millis() - start;
}
return {button, held};
}

View File

@ -1,28 +0,0 @@
#pragma once
// 4 buttons on ADC resistor ladder: Back, Confirm, Left, Right
#define BTN_GPIO1 1
// 2 buttons on ADC resistor ladder: Volume Up, Volume Down
#define BTN_GPIO2 2
// Power button (digital)
#define BTN_GPIO3 3
// Button ADC thresholds
#define BTN_THRESHOLD 100 // Threshold tolerance
#define BTN_RIGHT_VAL 3
#define BTN_LEFT_VAL 1500
#define BTN_CONFIRM_VAL 2700
#define BTN_BACK_VAL 3550
#define BTN_VOLUME_DOWN_VAL 3
#define BTN_VOLUME_UP_VAL 2305
enum Button { NONE = 0, RIGHT, LEFT, CONFIRM, BACK, VOLUME_UP, VOLUME_DOWN, POWER };
struct Input {
Button button;
unsigned long pressTime;
};
void setupInputPinModes();
Button getPressedButton();
Input getInput(long maxHoldMs = -1);

View File

@ -2,12 +2,12 @@
#include <EpdRenderer.h> #include <EpdRenderer.h>
#include <Epub.h> #include <Epub.h>
#include <GxEPD2_BW.h> #include <GxEPD2_BW.h>
#include <InputManager.h>
#include <SD.h> #include <SD.h>
#include <SPI.h> #include <SPI.h>
#include "Battery.h" #include "Battery.h"
#include "CrossPointState.h" #include "CrossPointState.h"
#include "Input.h"
#include "screens/BootLogoScreen.h" #include "screens/BootLogoScreen.h"
#include "screens/EpubReaderScreen.h" #include "screens/EpubReaderScreen.h"
#include "screens/FileSelectionScreen.h" #include "screens/FileSelectionScreen.h"
@ -30,6 +30,7 @@
GxEPD2_BW<GxEPD2_426_GDEQ0426T82, GxEPD2_426_GDEQ0426T82::HEIGHT> display(GxEPD2_426_GDEQ0426T82(EPD_CS, EPD_DC, GxEPD2_BW<GxEPD2_426_GDEQ0426T82, GxEPD2_426_GDEQ0426T82::HEIGHT> display(GxEPD2_426_GDEQ0426T82(EPD_CS, EPD_DC,
EPD_RST, EPD_BUSY)); EPD_RST, EPD_BUSY));
InputManager inputManager;
auto renderer = new EpdRenderer(&display); auto renderer = new EpdRenderer(&display);
Screen* currentScreen; Screen* currentScreen;
CrossPointState* appState; CrossPointState* appState;
@ -72,36 +73,55 @@ void enterNewScreen(Screen* screen) {
void verifyWakeupLongPress() { void verifyWakeupLongPress() {
// Give the user up to 1000ms to start holding the power button, and must hold for POWER_BUTTON_WAKEUP_MS // Give the user up to 1000ms to start holding the power button, and must hold for POWER_BUTTON_WAKEUP_MS
const auto start = millis(); const auto start = millis();
auto input = getInput(POWER_BUTTON_WAKEUP_MS); bool abort = false;
while (input.button != POWER && millis() - start < 1000) {
Serial.println("Verifying power button press");
inputManager.update();
while (!inputManager.isPressed(InputManager::BTN_POWER) && millis() - start < 1000) {
delay(50); delay(50);
input = getInput(POWER_BUTTON_WAKEUP_MS); inputManager.update();
Serial.println("Waiting...");
} }
if (input.button != POWER || input.pressTime < POWER_BUTTON_WAKEUP_MS) { Serial.printf("Made it? %s\n", inputManager.isPressed(InputManager::BTN_POWER) ? "yes" : "no");
if (inputManager.isPressed(InputManager::BTN_POWER)) {
do {
delay(50);
inputManager.update();
} while (inputManager.isPressed(InputManager::BTN_POWER) && inputManager.getHeldTime() < POWER_BUTTON_WAKEUP_MS);
abort = inputManager.getHeldTime() < POWER_BUTTON_WAKEUP_MS;
} else {
abort = true;
}
Serial.printf("held for %lu\n", inputManager.getHeldTime());
if (abort) {
// Button released too early. Returning to sleep. // Button released too early. Returning to sleep.
// IMPORTANT: Re-arm the wakeup trigger before sleeping again // IMPORTANT: Re-arm the wakeup trigger before sleeping again
esp_deep_sleep_enable_gpio_wakeup(1ULL << BTN_GPIO3, ESP_GPIO_WAKEUP_GPIO_LOW); esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
esp_deep_sleep_start(); esp_deep_sleep_start();
} }
} }
void waitForNoButton() { void waitForPowerRelease() {
while (getInput().button != NONE) { inputManager.update();
while (inputManager.isPressed(InputManager::BTN_POWER)) {
delay(50); delay(50);
inputManager.update();
} }
} }
// Enter deep sleep mode // Enter deep sleep mode
void enterDeepSleep() { void enterDeepSleep() {
exitScreen(); exitScreen();
enterNewScreen(new SleepScreen(renderer)); enterNewScreen(new SleepScreen(renderer, inputManager));
Serial.println("Power button released after a long press. Entering deep sleep."); Serial.println("Power button released after a long press. Entering deep sleep.");
delay(1000); // Allow Serial buffer to empty and display to update delay(1000); // Allow Serial buffer to empty and display to update
// Enable Wakeup on LOW (button press) // Enable Wakeup on LOW (button press)
esp_deep_sleep_enable_gpio_wakeup(1ULL << BTN_GPIO3, ESP_GPIO_WAKEUP_GPIO_LOW); esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
display.hibernate(); display.hibernate();
@ -112,17 +132,17 @@ void enterDeepSleep() {
void onGoHome(); void onGoHome();
void onSelectEpubFile(const std::string& path) { void onSelectEpubFile(const std::string& path) {
exitScreen(); exitScreen();
enterNewScreen(new FullScreenMessageScreen(renderer, "Loading...")); enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Loading..."));
Epub* epub = loadEpub(path); Epub* epub = loadEpub(path);
if (epub) { if (epub) {
appState->openEpubPath = path; appState->openEpubPath = path;
appState->saveToFile(); appState->saveToFile();
exitScreen(); exitScreen();
enterNewScreen(new EpubReaderScreen(renderer, epub, onGoHome)); enterNewScreen(new EpubReaderScreen(renderer, inputManager, epub, onGoHome));
} else { } else {
exitScreen(); exitScreen();
enterNewScreen(new FullScreenMessageScreen(renderer, "Failed to load epub", REGULAR, false, false)); enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Failed to load epub", REGULAR, false, false));
delay(2000); delay(2000);
onGoHome(); onGoHome();
} }
@ -130,11 +150,11 @@ void onSelectEpubFile(const std::string& path) {
void onGoHome() { void onGoHome() {
exitScreen(); exitScreen();
enterNewScreen(new FileSelectionScreen(renderer, onSelectEpubFile)); enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile));
} }
void setup() { void setup() {
setupInputPinModes(); inputManager.begin();
verifyWakeupLongPress(); verifyWakeupLongPress();
// Begin serial only if USB connected // Begin serial only if USB connected
@ -157,7 +177,7 @@ void setup() {
Serial.println("Display initialized"); Serial.println("Display initialized");
exitScreen(); exitScreen();
enterNewScreen(new BootLogoScreen(renderer)); enterNewScreen(new BootLogoScreen(renderer, inputManager));
// SD Card Initialization // SD Card Initialization
SD.begin(SD_SPI_CS, SPI, SPI_FQ); SD.begin(SD_SPI_CS, SPI, SPI_FQ);
@ -167,37 +187,31 @@ void setup() {
Epub* epub = loadEpub(appState->openEpubPath); Epub* epub = loadEpub(appState->openEpubPath);
if (epub) { if (epub) {
exitScreen(); exitScreen();
enterNewScreen(new EpubReaderScreen(renderer, epub, onGoHome)); enterNewScreen(new EpubReaderScreen(renderer, inputManager, epub, onGoHome));
// Ensure we're not still holding the power button before leaving setup // Ensure we're not still holding the power button before leaving setup
waitForNoButton(); waitForPowerRelease();
return; return;
} }
} }
exitScreen(); exitScreen();
enterNewScreen(new FileSelectionScreen(renderer, onSelectEpubFile)); enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile));
// Ensure we're not still holding the power button before leaving setup // Ensure we're not still holding the power button before leaving setup
waitForNoButton(); waitForPowerRelease();
} }
void loop() { void loop() {
delay(50); delay(10);
const Input input = getInput(); inputManager.update();
if (inputManager.wasReleased(InputManager::BTN_POWER) && inputManager.getHeldTime() > POWER_BUTTON_WAKEUP_MS) {
if (input.button == NONE) {
return;
}
if (input.button == POWER && input.pressTime > POWER_BUTTON_SLEEP_MS) {
enterDeepSleep(); enterDeepSleep();
// This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start // This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start
delay(1000);
return; return;
} }
if (currentScreen) { if (currentScreen) {
currentScreen->handleInput(input); currentScreen->handleInput();
} }
} }

View File

@ -3,6 +3,6 @@
class BootLogoScreen final : public Screen { class BootLogoScreen final : public Screen {
public: public:
explicit BootLogoScreen(EpdRenderer* renderer) : Screen(renderer) {} explicit BootLogoScreen(EpdRenderer* renderer, InputManager& inputManager) : Screen(renderer, inputManager) {}
void onEnter() override; void onEnter() override;
}; };

View File

@ -59,9 +59,36 @@ void EpubReaderScreen::onExit() {
epub = nullptr; epub = nullptr;
} }
void EpubReaderScreen::handleInput(const Input input) { void EpubReaderScreen::handleInput() {
if (input.button == VOLUME_UP || input.button == VOLUME_DOWN || input.button == LEFT || input.button == RIGHT) { if (inputManager.wasPressed(InputManager::BTN_BACK)) {
const bool skipChapter = input.pressTime > SKIP_CHAPTER_MS; onGoHome();
return;
}
const bool prevReleased =
inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT);
const bool nextReleased =
inputManager.wasReleased(InputManager::BTN_DOWN) || inputManager.wasReleased(InputManager::BTN_RIGHT);
if (!prevReleased && !nextReleased) {
return;
}
Serial.printf("Prev released: %d, Next released: %d\n", prevReleased, nextReleased);
const bool skipChapter = inputManager.getHeldTime() > SKIP_CHAPTER_MS;
if (skipChapter) {
// We don't want to delete the section mid-render, so grab the semaphore
xSemaphoreTake(renderingMutex, portMAX_DELAY);
nextPageNumber = 0;
currentSpineIndex = nextReleased ? currentSpineIndex + 1 : currentSpineIndex - 1;
delete section;
section = nullptr;
xSemaphoreGive(renderingMutex);
updateRequired = true;
return;
}
// No current section, attempt to rerender the book // No current section, attempt to rerender the book
if (!section) { if (!section) {
@ -69,17 +96,7 @@ void EpubReaderScreen::handleInput(const Input input) {
return; return;
} }
if ((input.button == VOLUME_UP || input.button == LEFT) && skipChapter) { if (prevReleased) {
nextPageNumber = 0;
currentSpineIndex--;
delete section;
section = nullptr;
} else if ((input.button == VOLUME_DOWN || input.button == RIGHT) && skipChapter) {
nextPageNumber = 0;
currentSpineIndex++;
delete section;
section = nullptr;
} else if (input.button == VOLUME_UP || input.button == LEFT) {
if (section->currentPage > 0) { if (section->currentPage > 0) {
section->currentPage--; section->currentPage--;
} else { } else {
@ -91,7 +108,8 @@ void EpubReaderScreen::handleInput(const Input input) {
section = nullptr; section = nullptr;
xSemaphoreGive(renderingMutex); xSemaphoreGive(renderingMutex);
} }
} else if (input.button == VOLUME_DOWN || input.button == RIGHT) { updateRequired = true;
} else {
if (section->currentPage < section->pageCount - 1) { if (section->currentPage < section->pageCount - 1) {
section->currentPage++; section->currentPage++;
} else { } else {
@ -103,11 +121,7 @@ void EpubReaderScreen::handleInput(const Input input) {
section = nullptr; section = nullptr;
xSemaphoreGive(renderingMutex); xSemaphoreGive(renderingMutex);
} }
}
updateRequired = true; updateRequired = true;
} else if (input.button == BACK) {
onGoHome();
} }
} }

View File

@ -24,9 +24,10 @@ class EpubReaderScreen final : public Screen {
void renderStatusBar() const; void renderStatusBar() const;
public: public:
explicit EpubReaderScreen(EpdRenderer* renderer, Epub* epub, const std::function<void()>& onGoHome) explicit EpubReaderScreen(EpdRenderer* renderer, InputManager& inputManager, Epub* epub,
: Screen(renderer), epub(epub), onGoHome(onGoHome) {} const std::function<void()>& onGoHome)
: Screen(renderer, inputManager), epub(epub), onGoHome(onGoHome) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void handleInput(Input input) override; void handleInput() override;
}; };

View File

@ -59,14 +59,13 @@ void FileSelectionScreen::onExit() {
files.clear(); files.clear();
} }
void FileSelectionScreen::handleInput(const Input input) { void FileSelectionScreen::handleInput() {
if (input.button == VOLUME_DOWN || input.button == RIGHT) { const bool prevPressed =
selectorIndex = (selectorIndex + 1) % files.size(); inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT);
updateRequired = true; const bool nextPressed =
} else if (input.button == VOLUME_UP || input.button == LEFT) { inputManager.wasPressed(InputManager::BTN_DOWN) || inputManager.wasPressed(InputManager::BTN_RIGHT);
selectorIndex = (selectorIndex + files.size() - 1) % files.size();
updateRequired = true; if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
} else if (input.button == CONFIRM) {
if (files.empty()) { if (files.empty()) {
return; return;
} }
@ -79,11 +78,17 @@ void FileSelectionScreen::handleInput(const Input input) {
} else { } else {
onSelect(basepath + files[selectorIndex]); onSelect(basepath + files[selectorIndex]);
} }
} else if (input.button == BACK && basepath != "/") { } else if (inputManager.wasPressed(InputManager::BTN_BACK) && basepath != "/") {
basepath = basepath.substr(0, basepath.rfind('/')); basepath = basepath.substr(0, basepath.rfind('/'));
if (basepath.empty()) basepath = "/"; if (basepath.empty()) basepath = "/";
loadFiles(); loadFiles();
updateRequired = true; updateRequired = true;
} else if (prevPressed) {
selectorIndex = (selectorIndex + files.size() - 1) % files.size();
updateRequired = true;
} else if (nextPressed) {
selectorIndex = (selectorIndex + 1) % files.size();
updateRequired = true;
} }
} }

View File

@ -24,9 +24,10 @@ class FileSelectionScreen final : public Screen {
void loadFiles(); void loadFiles();
public: public:
explicit FileSelectionScreen(EpdRenderer* renderer, const std::function<void(const std::string&)>& onSelect) explicit FileSelectionScreen(EpdRenderer* renderer, InputManager& inputManager,
: Screen(renderer), onSelect(onSelect) {} const std::function<void(const std::string&)>& onSelect)
: Screen(renderer, inputManager), onSelect(onSelect) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void handleInput(Input input) override; void handleInput() override;
}; };

View File

@ -12,8 +12,13 @@ class FullScreenMessageScreen final : public Screen {
bool partialUpdate; bool partialUpdate;
public: public:
explicit FullScreenMessageScreen(EpdRenderer* renderer, std::string text, const EpdFontStyle style = REGULAR, explicit FullScreenMessageScreen(EpdRenderer* renderer, InputManager& inputManager, std::string text,
const bool invert = false, const bool partialUpdate = true) const EpdFontStyle style = REGULAR, const bool invert = false,
: Screen(renderer), text(std::move(text)), style(style), invert(invert), partialUpdate(partialUpdate) {} const bool partialUpdate = true)
: Screen(renderer, inputManager),
text(std::move(text)),
style(style),
invert(invert),
partialUpdate(partialUpdate) {}
void onEnter() override; void onEnter() override;
}; };

View File

@ -1,16 +1,17 @@
#pragma once #pragma once
#include "Input.h" #include <InputManager.h>
class EpdRenderer; class EpdRenderer;
class Screen { class Screen {
protected: protected:
EpdRenderer* renderer; EpdRenderer* renderer;
InputManager& inputManager;
public: public:
explicit Screen(EpdRenderer* renderer) : renderer(renderer) {} explicit Screen(EpdRenderer* renderer, InputManager& inputManager) : renderer(renderer), inputManager(inputManager) {}
virtual ~Screen() = default; virtual ~Screen() = default;
virtual void onEnter() {} virtual void onEnter() {}
virtual void onExit() {} virtual void onExit() {}
virtual void handleInput(Input input) {} virtual void handleInput() {}
}; };

View File

@ -3,6 +3,6 @@
class SleepScreen final : public Screen { class SleepScreen final : public Screen {
public: public:
explicit SleepScreen(EpdRenderer* renderer) : Screen(renderer) {} explicit SleepScreen(EpdRenderer* renderer, InputManager& inputManager) : Screen(renderer, inputManager) {}
void onEnter() override; void onEnter() override;
}; };