From f593af6acd327b819495bb2a7e5b054fff1bc6ea Mon Sep 17 00:00:00 2001 From: Dmitry Shishkin Date: Tue, 17 Sep 2024 21:22:10 +0200 Subject: [PATCH 1/4] Use ASCII symbols for control commands (#202) Co-authored-by: forntoh --- docs/classes.puml | 49 +++ examples/KeyboardAdapter/KeyboardAdapter.ino | 45 +++ examples/SimpleInput/SimpleInput.ino | 4 +- src/ItemCommand.h | 11 +- src/ItemInput.h | 81 ++--- src/ItemInputCharset.h | 67 +++-- src/ItemList.h | 45 ++- src/ItemProgress.h | 45 ++- src/ItemToggle.h | 27 +- src/LcdMenu.h | 301 ++++++++----------- src/MenuItem.h | 28 +- src/input/InputInterface.h | 19 ++ src/input/KeyboardAdapter.h | 257 ++++++++++++++++ src/utils/RotaryNavConfig.h | 10 +- src/utils/SimpleNavConfig.h | 17 +- src/utils/constants.h | 11 + 16 files changed, 717 insertions(+), 300 deletions(-) create mode 100644 docs/classes.puml create mode 100644 examples/KeyboardAdapter/KeyboardAdapter.ino create mode 100644 src/input/InputInterface.h create mode 100644 src/input/KeyboardAdapter.h diff --git a/docs/classes.puml b/docs/classes.puml new file mode 100644 index 00000000..45334625 --- /dev/null +++ b/docs/classes.puml @@ -0,0 +1,49 @@ +@startuml +' https://www.plantuml.com/plantuml/uml/ + +hide empty members + +class LcdMenu { + +virtual bool handle(char c) + #virtual bool up() + #virtual bool down() + #virtual bool enter() + #virtual bool back() +} + +class MenuItem { + +virtual bool handle(char c) +} + +class ItemCommand { + +bool handle(char c) override + #bool enter() override +} + +class ItemList { + +bool handle(char c) override + #bool up() override + #bool down() override + #bool enter() override + #bool back() override +} + +class ItemInput { + +bool handle(char c) override + #bool up() override + #bool down() override + #bool enter() override + #bool back() override + #bool left() override + #bool right() override + #bool backspace() override + #bool typeChar(char c) override + #bool clear() override +} + +LcdMenu::handle -r-> MenuItem::handle +ItemCommand -u-|> MenuItem +ItemList -u-|> MenuItem +ItemInput -u-|> MenuItem + +@enduml \ No newline at end of file diff --git a/examples/KeyboardAdapter/KeyboardAdapter.ino b/examples/KeyboardAdapter/KeyboardAdapter.ino new file mode 100644 index 00000000..0bd03571 --- /dev/null +++ b/examples/KeyboardAdapter/KeyboardAdapter.ino @@ -0,0 +1,45 @@ +/* + * Usage example of `KeyboardAdapter`. + */ +#define DEBUG + +#include +#include +#include +#include + +#define LCD_ROWS 2 +#define LCD_COLS 16 + +// Create your charset +const char* charset = "0123456789"; + +// Declare the call back function +void inputCallback(char* value); + +MAIN_MENU( + ITEM_INPUT_CHARSET("Con", "0123456", charset, inputCallback), + ITEM_BASIC("Connect to WiFi"), + ITEM_BASIC("Blink SOS"), + ITEM_BASIC("Blink random")); + +LiquidCrystalI2CAdapter lcdAdapter(0x27, LCD_COLS, LCD_ROWS); +LcdMenu menu(lcdAdapter); +KeyboardAdapter keyboard(&menu, &Serial); + +void setup() { + Serial.begin(9600); + menu.initialize(mainMenu); +} + +void loop() { + keyboard.observe(); +} +/** + * Define callback + */ +void inputCallback(char* value) { + // Do stuff with value + Serial.print(F("# ")); + Serial.println(value); +} diff --git a/examples/SimpleInput/SimpleInput.ino b/examples/SimpleInput/SimpleInput.ino index 87f02ad4..7b771ea1 100644 --- a/examples/SimpleInput/SimpleInput.ino +++ b/examples/SimpleInput/SimpleInput.ino @@ -47,7 +47,7 @@ void loop() { char command = Serial.read(); if (!processWithSimpleCommand(&navConfig, command) && command != '\n') { - menu.type(command); + menu.process(command); } } /** @@ -56,4 +56,4 @@ void loop() { void inputCallback(char* value) { Serial.print(F("# ")); Serial.println(value); -} \ No newline at end of file +} diff --git a/src/ItemCommand.h b/src/ItemCommand.h index c7d28ef4..ea178fd4 100644 --- a/src/ItemCommand.h +++ b/src/ItemCommand.h @@ -48,10 +48,19 @@ class ItemCommand : public MenuItem { */ void setCallBack(fptr callback) { this->callback = callback; }; - void enter() override { + bool process(const unsigned char c) override { + switch (c) { + case ENTER: return enter(); + default: return false; + } + } + + protected: + bool enter() { if (callback != NULL) { callback(); } + return true; }; }; diff --git a/src/ItemInput.h b/src/ItemInput.h index dc480ce9..0eb06e7d 100644 --- a/src/ItemInput.h +++ b/src/ItemInput.h @@ -18,9 +18,7 @@ * @brief Item that allows user to input string information. * * ┌────────────────────────────┐ - * │ . . . │ * │ > T E X T : V A L U E │ - * │ . . . │ * └────────────────────────────┘ * * Additionally to `text` this item has string `value`. @@ -141,13 +139,34 @@ class ItemInput : public MenuItem { */ fptrStr getCallbackStr() { return callback; } - void up() override {} + bool process(const unsigned char c) override { + if (isprint(c)) { + return typeChar(c); + } + switch (c) { + case ENTER: return enter(); + case BACK: return back(); + case UP: return display->getEditModeEnabled(); + case DOWN: return display->getEditModeEnabled(); + case LEFT: return left(); + case RIGHT: return right(); + case BACKSPACE: return backspace(); + case CLEAR: return clear(); + default: return false; + } + } - void down() override {} + void draw(uint8_t row) override { + uint8_t maxCols = display->getMaxCols(); + static char* buf = new char[maxCols]; + substring(value, view, viewSize, buf); + display->drawItem(row, text, ':', buf); + } - void enter() override { + protected: + bool enter() { if (display->getEditModeEnabled()) { - return; + return false; } // Move cursor to the latest index uint8_t length = strlen(value); @@ -161,11 +180,11 @@ class ItemInput : public MenuItem { display->setEditModeEnabled(true); display->resetBlinker(constrainBlinkerPosition(strlen(text) + 2 + cursor - view)); display->drawBlinker(); + return true; }; - - void back() override { + bool back() { if (!display->getEditModeEnabled()) { - return; + return false; } display->clearBlinker(); display->setEditModeEnabled(false); @@ -176,14 +195,14 @@ class ItemInput : public MenuItem { if (callback != NULL) { callback(value); } + return true; }; - - void left() override { + bool left() { if (!display->getEditModeEnabled()) { - return; + return false; } if (cursor == 0) { - return; + return true; } cursor--; if (cursor < view) { @@ -193,14 +212,14 @@ class ItemInput : public MenuItem { display->resetBlinker(constrainBlinkerPosition(display->getBlinkerPosition() - 1)); // Log printCmd(F("LEFT"), value[display->getBlinkerPosition() - (strlen(text) + 2)]); + return true; }; - - void right() override { + bool right() { if (!display->getEditModeEnabled()) { - return; + return false; } if (cursor == strlen(value)) { - return; + return true; } cursor++; if (cursor > (view + viewSize - 1)) { @@ -210,15 +229,8 @@ class ItemInput : public MenuItem { display->resetBlinker(constrainBlinkerPosition(display->getBlinkerPosition() + 1)); // Log printCmd(F("RIGHT"), value[display->getBlinkerPosition() - (strlen(text) + 2)]); + return true; }; - - void draw(uint8_t row) override { - uint8_t maxCols = display->getMaxCols(); - static char* buf = new char[maxCols]; - substring(value, view, viewSize, buf); - display->drawItem(row, text, ':', buf); - } - /** * Execute a "backspace cmd" on menu * @@ -226,12 +238,12 @@ class ItemInput : public MenuItem { * * Removes the character at the current cursor position. */ - void backspace() override { + bool backspace() { if (!display->getEditModeEnabled()) { - return; + return false; } if (strlen(value) == 0 || cursor == 0) { - return; + return true; } remove(value, cursor - 1, 1); printCmd(F("BACKSPACE"), value); @@ -241,16 +253,16 @@ class ItemInput : public MenuItem { } MenuItem::draw(); display->resetBlinker(constrainBlinkerPosition(display->getBlinkerPosition() - 1)); + return true; } - /** * Display text at the cursor position * used for `Input` type menu items * @param character character to append */ - void typeChar(const char character) override { + bool typeChar(const char character) { if (!display->getEditModeEnabled()) { - return; + return false; } uint8_t length = strlen(value); if (cursor < length) { @@ -274,14 +286,14 @@ class ItemInput : public MenuItem { display->resetBlinker(constrainBlinkerPosition(display->getBlinkerPosition() + 1)); // Log printCmd(F("TYPE-CHAR"), character); + return true; } - /** * Clear the value of the input field */ - void clear() override { + bool clear() { if (!display->getEditModeEnabled()) { - return; + return false; } // // set the value @@ -294,6 +306,7 @@ class ItemInput : public MenuItem { // MenuItem::draw(); display->resetBlinker(constrainBlinkerPosition(strlen(text) + 2)); + return true; } }; diff --git a/src/ItemInputCharset.h b/src/ItemInputCharset.h index e423ed9a..ae9dd116 100644 --- a/src/ItemInputCharset.h +++ b/src/ItemInputCharset.h @@ -1,9 +1,8 @@ #ifndef ItemInputCharset_H #define ItemInputCharset_H -#include - #include "ItemInput.h" +#include class ItemInputCharset : public ItemInput { private: @@ -48,13 +47,27 @@ class ItemInputCharset : public ItemInput { ItemInputCharset(const char* text, const char* charset, fptrStr callback) : ItemInputCharset(text, (char*)"", charset, callback) {} - void enter() override { + bool process(const unsigned char c) override { + switch (c) { + case ENTER: return enter(); + case BACK: return back(); + case UP: return up(); + case DOWN: return down(); + case LEFT: return left(); + case RIGHT: return right(); + case BACKSPACE: return ItemInput::backspace(); + case CLEAR: return ItemInput::clear(); + default: return false; + } + } + + protected: + bool enter() { if (!display->getEditModeEnabled()) { - ItemInput::enter(); - return; + return ItemInput::enter(); } if (!charEditMode) { - return; + return true; } uint8_t length = strlen(value); if (cursor < length) { @@ -67,70 +80,62 @@ class ItemInputCharset : public ItemInput { printCmd(F("CHARSET"), value); stopCharsetEditMode(); ItemInput::right(); + return true; } - void back() override { + bool back() { if (!display->getEditModeEnabled()) { - return; + return false; } if (!charEditMode) { - ItemInput::back(); - return; + return ItemInput::back(); } stopCharsetEditMode(); + return true; }; - void left() override { + bool left() { if (!display->getEditModeEnabled()) { - return; + return false; } if (charEditMode) { stopCharsetEditMode(); } - ItemInput::left(); + return ItemInput::left(); } - void right() override { + bool right() { if (!display->getEditModeEnabled()) { - return; + return false; } if (charEditMode) { stopCharsetEditMode(); } - ItemInput::right(); + return ItemInput::right(); } - void down() override { + bool down() { if (!display->getEditModeEnabled()) { - return; + return false; } if (!charEditMode) { initCharsetEditMode(); } charsetPosition = (charsetPosition + 1) % charsetSize; display->drawChar(charset[charsetPosition]); + return true; } - void up() override { + bool up() { if (!display->getEditModeEnabled()) { - return; + return false; } if (!charEditMode) { initCharsetEditMode(); } charsetPosition = constrain(charsetPosition - 1, 0, charsetSize); display->drawChar(charset[charsetPosition]); - } - - void clear() override { - if (!display->getEditModeEnabled()) { - return; - } - ItemInput::clear(); - } - - void typeChar(const char character) override { - // Do nothing + return true; } }; diff --git a/src/ItemList.h b/src/ItemList.h index 3188e874..f02037f0 100644 --- a/src/ItemList.h +++ b/src/ItemList.h @@ -85,26 +85,46 @@ class ItemList : public MenuItem { */ String* getItems() { return items; } - void enter() override { + void draw(uint8_t row) override { + uint8_t maxCols = display->getMaxCols(); + static char* buf = new char[maxCols]; + substring(items[itemIndex].c_str(), 0, maxCols - strlen(text) - 2, buf); + display->drawItem(row, text, ':', buf); + } + + bool process(const unsigned char c) override { + switch (c) { + case ENTER: return enter(); + case BACK: return back(); + case UP: return up(); + case DOWN: return down(); + default: return false; + } + } + + protected: + bool enter() { if (display->getEditModeEnabled()) { - return; + return false; } display->setEditModeEnabled(true); + return true; }; - void back() override { + bool back() { if (!display->getEditModeEnabled()) { - return; + return false; } display->setEditModeEnabled(false); if (callback != NULL) { callback(itemIndex); } + return true; }; - void down() override { + bool down() { if (!display->getEditModeEnabled()) { - return; + return false; } uint8_t previousIndex = itemIndex; itemIndex = constrain(itemIndex - 1, 0, (uint16_t)(itemCount)-1); @@ -112,11 +132,12 @@ class ItemList : public MenuItem { printCmd(F("LEFT"), items[itemIndex].c_str()); MenuItem::draw(); } + return true; }; - void up() override { + bool up() { if (!display->getEditModeEnabled()) { - return; + return false; } uint8_t previousIndex = itemIndex; itemIndex = constrain((itemIndex + 1) % itemCount, 0, (uint16_t)(itemCount)-1); @@ -124,14 +145,8 @@ class ItemList : public MenuItem { printCmd(F("RIGHT"), items[itemIndex].c_str()); MenuItem::draw(); } + return true; }; - - void draw(uint8_t row) override { - uint8_t maxCols = display->getMaxCols(); - static char* buf = new char[maxCols]; - substring(items[itemIndex].c_str(), 0, maxCols - strlen(text) - 2, buf); - display->drawItem(row, text, ':', buf); - } }; #define ITEM_STRING_LIST(...) (new ItemList(__VA_ARGS__)) diff --git a/src/ItemProgress.h b/src/ItemProgress.h index e73f6dbb..f101320f 100644 --- a/src/ItemProgress.h +++ b/src/ItemProgress.h @@ -103,26 +103,46 @@ class ItemProgress : public MenuItem { return mapping(progress); } - void enter() override { + void draw(uint8_t row) override { + uint8_t maxCols = display->getMaxCols(); + static char* buf = new char[maxCols]; + substring(getValue(), 0, maxCols - strlen(text) - 2, buf); + display->drawItem(row, text, ':', buf); + } + + bool process(const unsigned char c) override { + switch (c) { + case ENTER: return enter(); + case BACK: return back(); + case UP: return up(); + case DOWN: return down(); + default: return false; + } + } + + protected: + bool enter() { if (display->getEditModeEnabled()) { - return; + return false; } display->setEditModeEnabled(true); + return true; }; - void back() override { + bool back() { if (!display->getEditModeEnabled()) { - return; + return false; } display->setEditModeEnabled(false); if (callback != NULL) { callback(progress); } + return true; }; - void down() override { + bool down() { if (!display->getEditModeEnabled()) { - return; + return false; } uint16_t oldProgress = progress; decrement(); @@ -130,11 +150,12 @@ class ItemProgress : public MenuItem { printCmd(F("LEFT"), getValue()); MenuItem::draw(); } + return true; }; - void up() override { + bool up() { if (!display->getEditModeEnabled()) { - return; + return false; } uint16_t oldProgress = progress; increment(); @@ -142,14 +163,8 @@ class ItemProgress : public MenuItem { printCmd(F("RIGHT"), getValue()); MenuItem::draw(); } + return true; }; - - void draw(uint8_t row) override { - uint8_t maxCols = display->getMaxCols(); - static char* buf = new char[maxCols]; - substring(getValue(), 0, maxCols - strlen(text) - 2, buf); - display->drawItem(row, text, ':', buf); - } }; #define ITEM_PROGRESS(...) (new ItemProgress(__VA_ARGS__)) diff --git a/src/ItemToggle.h b/src/ItemToggle.h index 221ef63d..2d3aba55 100644 --- a/src/ItemToggle.h +++ b/src/ItemToggle.h @@ -16,9 +16,7 @@ * @brief Item that allows user to toggle between ON/OFF states. * * ┌────────────────────────────┐ - * │ . . . │ * │ > T E X T : O F F │ - * │ . . . │ * └────────────────────────────┘ * * Additionally to `text` this item has ON/OFF `enabled` state. @@ -78,20 +76,29 @@ class ItemToggle : public MenuItem { const char* getTextOff() { return this->textOff; } - void enter() override { - enabled = !enabled; - if (callback != NULL) { - callback(enabled); - } - MenuItem::draw(); - }; - void draw(uint8_t row) override { uint8_t maxCols = display->getMaxCols(); static char* buf = new char[maxCols]; substring(enabled ? textOn : textOff, 0, maxCols - strlen(text) - 2, buf); display->drawItem(row, text, ':', buf); }; + + bool process(const unsigned char c) override { + switch (c) { + case ENTER: return enter(); + default: return false; + } + }; + + protected: + bool enter() { + enabled = !enabled; + if (callback != NULL) { + callback(enabled); + } + MenuItem::draw(); + return true; + }; }; #define ITEM_TOGGLE(...) (new ItemToggle(__VA_ARGS__)) diff --git a/src/LcdMenu.h b/src/LcdMenu.h index e195b4b4..4736991b 100644 --- a/src/LcdMenu.h +++ b/src/LcdMenu.h @@ -26,6 +26,7 @@ #pragma once #include "interface/DisplayInterface.h" +#include "utils/constants.h" #include #include @@ -83,34 +84,6 @@ class LcdMenu { lcd.drawCursor(); // In case if currentPosition was not changed between screens } - public: - /** - * ## Public Fields - */ - /** - * Display Interface - */ - DisplayInterface& lcd; - - /** - * # Constructor - */ - - LcdMenu(DisplayInterface& display) : lcd(display) { - bottom = lcd.getMaxRows(); - maxRows = lcd.getMaxRows(); - maxCols = lcd.getMaxCols(); - } - - void initialize(MenuItem* menu[]) { - lcd.begin(); - currentMenuTable = menu; - for (uint8_t i = 1; currentMenuTable[i]->getType() != MENU_ITEM_END_OF_MENU; ++i) { - currentMenuTable[i]->initialize(&lcd); - } - drawMenu(); - lcd.drawCursor(); - } /* * Draw the menu items and cursor */ @@ -121,8 +94,9 @@ class LcdMenu { drawMenu(); lcd.drawCursor(); } + /* - * Draw the menu items and cursor + * Draw the cursor */ void updateOnlyCursor() { if (!enableUpdate) { @@ -131,14 +105,6 @@ class LcdMenu { lcd.moveCursor(constrain(cursorPosition - top, 0, maxRows - 1)); } - inline bool isAtTheStart(uint8_t position) { - return position == 1; - } - - inline bool isAtTheEnd(uint8_t position) { - return currentMenuTable[position + 1]->getType() == MENU_ITEM_END_OF_MENU; - } - void drawMenu() { for (uint8_t i = top; i <= bottom; i++) { MenuItem* item = currentMenuTable[i]; @@ -159,24 +125,138 @@ class LcdMenu { } } + public: /** - * Reset the display + * ## Public Fields + */ + /** + * Display Interface + */ + DisplayInterface& lcd; + + /** + * # Constructor + */ + LcdMenu(DisplayInterface& display) : lcd(display) { + bottom = lcd.getMaxRows(); + maxRows = lcd.getMaxRows(); + maxCols = lcd.getMaxCols(); + } + + void initialize(MenuItem* menu[]) { + lcd.begin(); + currentMenuTable = menu; + for (uint8_t i = 1; currentMenuTable[i]->getType() != MENU_ITEM_END_OF_MENU; ++i) { + currentMenuTable[i]->initialize(&lcd); + } + drawMenu(); + lcd.drawCursor(); + } + + bool process(const unsigned char c) { + if (currentMenuTable[cursorPosition]->process(c)) { + return true; + } + switch (c) { + case UP: return up(); + case DOWN: return down(); + case ENTER: return enter(); + case BACK: return back(); + default: return false; + } + }; + + /** + * When you want to display any other content on the screen then + * call this function then display your content, later call + * `show()` to show the menu + */ + void hide() { + enableUpdate = false; + lcd.clear(); + } + /** + * Show the menu */ - void resetMenu() { this->reset(false); } + void show() { + enableUpdate = true; + update(); + } + /** + * @brief Checks if the given position is at the start. + * @param position The position to check. + * @return true if the position is at the start (i.e., equal to 1), + * false otherwise. + */ + inline bool isAtTheStart(uint8_t position) { + return position == 1; + } + /** + * @brief Checks if the specified position is at the end of the menu. + * @param position The index of the item to check. + * @return true if the next item is the end of the menu; false otherwise. + */ + inline bool isAtTheEnd(uint8_t position) { + return currentMenuTable[position + 1]->getType() == MENU_ITEM_END_OF_MENU; + } + /** + * Get the current cursor position + * @return `cursorPosition` e.g. 1, 2, 3... + */ + uint8_t getCursorPosition() { return this->cursorPosition; } + /** + * Set the current cursor position + * @param position + */ + void setCursorPosition(uint8_t position) { + uint8_t bottom = position; + bool isNotEnd = false; + + do { + isNotEnd = currentMenuTable[++bottom]->getType() != MENU_ITEM_END_OF_MENU && bottom < maxRows + 20; + } while (isNotEnd); + + uint8_t max = maxRows - 1; + + this->cursorPosition = position + 1; + this->bottom = constrain(cursorPosition + max, max, bottom - 1); + this->top = this->bottom - max; + } + /** + * Check if currently displayed menu is a sub menu. + */ + bool isSubMenu() { + byte menuItemType = currentMenuTable[0]->getType(); + return menuItemType == MENU_ITEM_SUB_MENU_HEADER; + } + /** + * Get a `MenuItem` at position + * @return `MenuItem` - item at `position` + */ + MenuItem* getItemAt(uint8_t position) { return currentMenuTable[position]; } + /** + * Get a `MenuItem` at position using operator function + * e.g `menu[menu.getCursorPosition()]` will return the item at the + * current cursor position NB: This is relative positioning (i.e. if a + * submenu is currently being displayed, menu[1] will return item 1 in + * the current menu) + * @return `MenuItem` - item at `position` + */ + MenuItem* operator[](const uint8_t position) { + return currentMenuTable[position]; + } + + protected: /** * Execute an "up press" on menu * When edit mode is enabled, this action is skipped */ - void up() { - if (lcd.getEditModeEnabled()) { - currentMenuTable[cursorPosition]->up(); - return; - } + bool up() { // // determine if cursor ia at start of menu items // if (isAtTheStart(cursorPosition)) { - return; + return false; } cursorPosition--; // Log @@ -194,21 +274,18 @@ class LcdMenu { } else { updateOnlyCursor(); } + return true; } /** * Execute a "down press" on menu * When edit mode is enabled, this action is skipped */ - void down() { - if (lcd.getEditModeEnabled()) { - currentMenuTable[cursorPosition]->down(); - return; - } + bool down() { // // determine if cursor has passed the end // if (isAtTheEnd(cursorPosition)) { - return; + return false; } cursorPosition++; // Log @@ -226,6 +303,7 @@ class LcdMenu { } else { updateOnlyCursor(); } + return true; } /** * Execute an "enter" action on menu. @@ -236,7 +314,7 @@ class LcdMenu { * - Execute a callback action. * - Toggle the state of an item. */ - void enter() { + bool enter() { MenuItem* item = currentMenuTable[cursorPosition]; // Log printCmd(F("ENTER"), item->getType()); @@ -252,137 +330,24 @@ class LcdMenu { // display the sub menu // reset(false); - return; } - item->enter(); + return true; } /** * Execute a "backpress" action on menu. * * Navigates up once. */ - void back() { + bool back() { // Log printCmd(F("BACK")); // - // Back action different when on ItemInput - // - if (lcd.getEditModeEnabled()) { - currentMenuTable[cursorPosition]->back(); - return; - } - // // check if this is a sub menu, if so go back to its parent // if (isSubMenu()) { currentMenuTable = currentMenuTable[0]->getSubMenu(); reset(true); } - } - /** - * Execute a "left press" on menu - * - * *NB: Works only for `ItemInput` and `ItemList` types* - * - * Moves the cursor one step to the left. - */ - void left() { - currentMenuTable[cursorPosition]->left(); - } - /** - * Execute a "right press" on menu - * - * *NB: Works only for `ItemInput` and `ItemList` types* - * - * Moves the cursor one step to the right. - */ - void right() { - currentMenuTable[cursorPosition]->right(); - } - /** - * Execute a "backspace cmd" on menu - * - * *NB: Works only for `ItemInput` type* - * - * Removes the character at the current cursor position. - */ - void backspace() { - currentMenuTable[cursorPosition]->backspace(); - } - /** - * Display text at the cursor position - * used for `Input` type menu items - * @param character character to append - */ - void type(const char character) { - currentMenuTable[cursorPosition]->typeChar(character); - } - /** - * Clear the value of the input field - */ - void clear() { - currentMenuTable[cursorPosition]->clear(); - } - /** - * When you want to display any other content on the screen then - * call this function then display your content, later call - * `show()` to show the menu - */ - void hide() { - enableUpdate = false; - lcd.clear(); - } - /** - * Show the menu - */ - void show() { - enableUpdate = true; - update(); - } - /** - * Get the current cursor position - * @return `cursorPosition` e.g. 1, 2, 3... - */ - uint8_t getCursorPosition() { return this->cursorPosition; } - /** - * Set the current cursor position - * @param position - */ - void setCursorPosition(uint8_t position) { - uint8_t bottom = position; - bool isNotEnd = false; - - do { - isNotEnd = currentMenuTable[++bottom]->getType() != MENU_ITEM_END_OF_MENU && bottom < maxRows + 20; - } while (isNotEnd); - - uint8_t max = maxRows - 1; - - this->cursorPosition = position + 1; - this->bottom = constrain(cursorPosition + max, max, bottom - 1); - this->top = this->bottom - max; - } - /** - * Check if currently displayed menu is a sub menu. - */ - bool isSubMenu() { - byte menuItemType = currentMenuTable[0]->getType(); - return menuItemType == MENU_ITEM_SUB_MENU_HEADER; - } - /** - * Get a `MenuItem` at position - * @return `MenuItem` - item at `position` - */ - MenuItem* getItemAt(uint8_t position) { return currentMenuTable[position]; } - /** - * Get a `MenuItem` at position using operator function - * e.g `menu[menu.getCursorPosition()]` will return the item at the - * current cursor position NB: This is relative positioning (i.e. if a - * submenu is currently being displayed, menu[1] will return item 1 in - * the current menu) - * @return `MenuItem` - item at `position` - */ - MenuItem* operator[](const uint8_t position) { - return currentMenuTable[position]; + return true; } }; diff --git a/src/MenuItem.h b/src/MenuItem.h index 73097c26..096361d0 100644 --- a/src/MenuItem.h +++ b/src/MenuItem.h @@ -32,7 +32,12 @@ #include /** - * The MenuItem class + * @brief The MenuItem class. + * + * ┌────────────────────────────┐ + * │ > T E X T │ + * └────────────────────────────┘ + * */ class MenuItem { protected: @@ -75,16 +80,17 @@ class MenuItem { * @param text text to display for the item */ void setText(const char* text) { this->text = text; }; - - virtual void up() {} - virtual void down() {} - virtual void enter() {} - virtual void back() {} - virtual void left() {} - virtual void right() {} - virtual void backspace() {} - virtual void typeChar(const char character) {} - virtual void clear() {} + /** + * @brief Process a command decoded in 1 byte. + * It can be a printable character or a control command like `ENTER` or `LEFT`. + * Return value is used to determine operation was successful or ignored. + * If the parent of item received that handle was ignored it can execute it's own action on this command. + * Thus, the item always has priority in processing; if it is ignored, it is delegated to the parent element. + * Behaviour is very similar to Even Bubbling in JavaScript. + * @param c the character, can be a printable character or a control command + * @return true if command was successfully handled by item. + */ + virtual bool process(const unsigned char c) { return false; }; virtual void draw() { draw(display->getCursorRow()); diff --git a/src/input/InputInterface.h b/src/input/InputInterface.h new file mode 100644 index 00000000..0d7f94d7 --- /dev/null +++ b/src/input/InputInterface.h @@ -0,0 +1,19 @@ +#pragma once + +#include "LcdMenu.h" + +/** + * @brief Input device for menu. + * Main goal is to hide implementation of input device(s) and convert from their events + * into 1 byte command to `LcdMenu`. + */ +class InputInterface { + protected: + LcdMenu* menu = NULL; + + public: + InputInterface(LcdMenu* menu) + : menu(menu) { + } + virtual void observe() = 0; +}; diff --git a/src/input/KeyboardAdapter.h b/src/input/KeyboardAdapter.h new file mode 100644 index 00000000..b86844ea --- /dev/null +++ b/src/input/KeyboardAdapter.h @@ -0,0 +1,257 @@ +#pragma once + +#include "InputInterface.h" +#include "Stream.h" + +#define BS 8 // Backspace, \b +#define LF 10 // Line feed, \n +#define CR 13 // Carriage Return, \r +#define ESC 27 // Escape +#define DEL 127 // Del, Backspace on Mac +#define C2_CSI_TERMINAL_MIN 0x40 +#define C2_CSI_TERMINAL_MAX 0x7E +#define THRESHOLD 100 +#define CSI_BUFFER_SIZE 5 + +/** + * @brief Input interface to keyboard. + * Uses default buttons layout: + * - `Arrows` for `LEFT/RIGHT/UP/DOWN`; + * - `Enter` for `ENTER`; + * - `Escape` for `BACK`; + * - `Delete` for `CLEAR`; + * - `Backspace` for `BACKSPACE`; + * + * Keyboard can send multiple-bytes commands. + * Implementation should convert it to one byte command. + * + * Implementation details. Mapping: + * `First 128 of ASCII` -> as is + * `\r` -> `ENTER` + * `\n` -> `ENTER` + * `\r\n` -> `ENTER` + * `ESC` -> `BACK` + * `ESC [ A` (up arrow) -> `UP` + * `ESC [ B` (down arrow) -> `DOWN` + * `ESC [ C` (right arrow) -> `RIGHT` + * `ESC [ D` (left arrow) -> `LEFT` + * `ESC [ 3 ~` (Delete button) -> `CLEAR` + */ +class KeyboardAdapter : public InputInterface { + private: + /** + * @see https://en.wikipedia.org/wiki/ANSI_escape_code + * @see https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797 + */ + enum class CodeSet { + /** + * @brief Main ASCII code set. + * Initially defined as part of ASCII, the default C0 control code set is now defined in ISO 6429 (ECMA-48). + */ + C0, + /** + * @brief Fe Escape sequences. + * Starts with ESC. + * Terminates with [0x40, 0x5F]. + */ + C1, + /** + * @brief Control Sequence Introducer. + * Starts with ESC [. + * Terminates with [0x40, 0x7E]. + */ + C2_CSI, + /** + * @brief Device Control String. + * Starts with ESC P. + * Terminates with ESC \. + */ + C2_DCS, + /** + * @brief Operating System Command. + * Starts with ESC ]. + * Terminates with ESC \. + * Currently not implemented. + */ + C2_OSC, + /** + * @brief Expect string terminator. + * Currently not implemented. + */ + C3, + }; + /** + * @brief Input stream. + */ + Stream* stream = NULL; + /** + * @brief Internal state of current code set. + * As stream receives bytes asynchronously, multiple bytes command can arrive + * in several calls. Need to store current state between calls. + */ + CodeSet codeSet = CodeSet::C0; + /** + * @brief Last received character. + * Used to detect 2 chars sequences, e.g. `\r\n`. + */ + unsigned char lastChar; + /** + * @brief Milliseconds timestamp of last received character. + * Used for detecting ESC with no chars next or single `\r` without `\n`. + */ + unsigned long lastCharTimestamp; + /** + * @brief Buffer to store already read values of "intermediate bytes". + * Multiple bytes command for CSI is in form of `ESC [ `. + */ + char csiBuffer[CSI_BUFFER_SIZE]; + /** + * @brief Points to next available byte in `csiBuffer`. + */ + uint8_t csiBufferCursor = 0; + /** + * @brief Reset to initial state. + */ + inline void reset() { + codeSet = CodeSet::C0; + csiBufferCursor = 0; + lastChar = 0; + lastCharTimestamp = 0; + } + inline bool hasLastChar() { + // TODO: Check millis() overflow situation + return lastChar != 0 && millis() > lastCharTimestamp + THRESHOLD; + } + inline void saveLastChar(unsigned char command) { + lastChar = command; + lastCharTimestamp = millis(); + } + /** + * @brief Handle idle state when there are no input for some time. + * @see THRESHOLD - timeout in ms. + */ + void handleIdle() { + switch (lastChar) { + case CR: // Received single `\r` + printCmd(F("Call ENTER from idle")); + menu->process(ENTER); + break; + case ESC: // Received single `ESC` + printCmd(F("Call BACK from idle")); + menu->process(BACK); + break; + } + } + /** + * @brief Handle received command. + * @param command the received command + */ + void handleReceived(unsigned char command) { + switch (codeSet) { + case CodeSet::C0: + switch (command) { + case BS: // 8. On Win + case DEL: // 127. On Mac + printCmd(F("Call BACKSPACE")); + menu->process(BACKSPACE); + break; + case LF: // 10, \n + printCmd(F("Call ENTER")); + menu->process(ENTER); + break; + case CR: // 13, \r + // Can be \r\n sequence, do nothing + break; + case ESC: // 27 + codeSet = CodeSet::C1; + break; + default: + printCmd(F("Call default"), (uint8_t)command); + menu->process(command); + break; + } + saveLastChar(command); + break; + case CodeSet::C1: + switch (command) { + case '[': + codeSet = CodeSet::C2_CSI; + break; + default: + reset(); // Reset after unsupported C1 command + break; + } + break; + case CodeSet::C2_CSI: + if (command >= C2_CSI_TERMINAL_MIN && command <= C2_CSI_TERMINAL_MAX) { + switch (command) { + case 'A': + printCmd(F("Call UP")); + menu->process(UP); + break; + case 'B': + printCmd(F("Call DOWN")); + menu->process(DOWN); + break; + case 'C': + printCmd(F("Call RIGHT")); + menu->process(RIGHT); + break; + case 'D': + printCmd(F("Call LEFT")); + menu->process(LEFT); + break; + case 'F': + printCmd(F("End")); + break; + case 'H': + printCmd(F("Home")); + break; + case '~': + if (csiBufferCursor > 0) { + switch (csiBuffer[0] - '0') { + case 2: // Insert + printCmd(F("Insert")); + break; + case 3: // Delete + printCmd(F("Call CLEAR")); + menu->process(CLEAR); + break; + case 5: // PgUp + printCmd(F("PgUp")); + break; + case 6: // PgDn + printCmd(F("PgDn")); + break; + } + } + } + reset(); // Reset after C2 terminal symbol + } else { + csiBuffer[csiBufferCursor] = command; + csiBufferCursor++; + } + break; + default: + reset(); // Reset after unknown code set + break; + } + }; + + public: + KeyboardAdapter(LcdMenu* menu, Stream* stream) + : InputInterface(menu), stream(stream) { + } + void observe() override { + if (!stream->available()) { + if (hasLastChar()) { + handleIdle(); + reset(); + } + return; + } + unsigned char command = stream->read(); + printCmd(F("INPUT"), (uint8_t)command); + handleReceived(command); + } +}; diff --git a/src/utils/RotaryNavConfig.h b/src/utils/RotaryNavConfig.h index 6ef6d5d6..4d01aa9b 100644 --- a/src/utils/RotaryNavConfig.h +++ b/src/utils/RotaryNavConfig.h @@ -31,9 +31,9 @@ void processWithRotaryEncoder(RotaryNavConfig* config) { // Handle rotary encoder rotation uint8_t rotation = config->encoder->rotate(); if (rotation == 1) { - config->menu->down(); // Call DOWN action + config->menu->process(DOWN); // Call DOWN action } else if (rotation == 2) { - config->menu->up(); // Call UP action + config->menu->process(UP); // Call UP action } // Handle button press (short, long, and double press) @@ -44,7 +44,7 @@ void processWithRotaryEncoder(RotaryNavConfig* config) { if (config->pendingEnter) { if (config->doublePressThreshold > 0 && currentTime - config->lastPressTime < config->doublePressThreshold) { - config->menu->backspace(); // Call BACKSPACE action (double press) + config->menu->process(BACKSPACE); // Call BACKSPACE action (double press) config->pendingEnter = false; } } else { @@ -52,14 +52,14 @@ void processWithRotaryEncoder(RotaryNavConfig* config) { config->lastPressTime = currentTime; } } else if (pressType == 2) { - config->menu->back(); // Call BACK action (long press) + config->menu->process(BACK); // Call BACK action (long press) config->pendingEnter = false; } // Check if the doublePressThreshold has elapsed for pending enter action if ((!config->menu->lcd.getEditModeEnabled() && config->pendingEnter) || (config->pendingEnter && (currentTime - config->lastPressTime >= config->doublePressThreshold))) { - config->menu->enter(); // Call ENTER action (short press) + config->menu->process(ENTER); // Call ENTER action (short press) config->pendingEnter = false; } } \ No newline at end of file diff --git a/src/utils/SimpleNavConfig.h b/src/utils/SimpleNavConfig.h index c971f7e6..00e769a2 100644 --- a/src/utils/SimpleNavConfig.h +++ b/src/utils/SimpleNavConfig.h @@ -18,24 +18,25 @@ struct SimpleNavConfig { bool processWithSimpleCommand(SimpleNavConfig* config, byte cmd) { if (config->up && cmd == config->up) { - config->menu->up(); + config->menu->process(UP); } else if (config->down && cmd == config->down) { - config->menu->down(); + config->menu->process(DOWN); } else if (config->left && cmd == config->left) { - config->menu->left(); + config->menu->process(LEFT); } else if (config->right && cmd == config->right) { - config->menu->right(); + config->menu->process(RIGHT); } else if (config->enter && cmd == config->enter) { - config->menu->enter(); + config->menu->process(ENTER); } else if (config->back && cmd == config->back) { - config->menu->back(); + config->menu->process(BACK); #ifdef ItemInput_H } else if (config->clear && cmd == config->clear) { - config->menu->clear(); + config->menu->process(CLEAR); } else if (config->backspace && cmd == config->backspace) { - config->menu->backspace(); + config->menu->process(BACKSPACE); #endif } else { + config->menu->process(cmd); return false; } return true; diff --git a/src/utils/constants.h b/src/utils/constants.h index 3a28cded..8c4a867b 100644 --- a/src/utils/constants.h +++ b/src/utils/constants.h @@ -19,6 +19,17 @@ const byte MENU_ITEM_END_OF_MENU = 8; const byte MENU_ITEM_LIST = 9; const byte MENU_ITEM_PROGRESS = 10; // +// Control codes +// +#define BACKSPACE 8 // Backspace +#define ENTER 10 // Enter +#define BACK 27 // Escape +#define UP 128 // >127 +#define DOWN 129 // >127 +#define RIGHT 130 // >127 +#define LEFT 131 // >127 +#define CLEAR 132 // >127 +// const byte DOWN_ARROW[8] = { 0b00100, // * 0b00100, // * From 78b85eb8d5e3174346e3bfcbf741450edb78f121 Mon Sep 17 00:00:00 2001 From: Thomas Forntoh Date: Wed, 18 Sep 2024 00:16:28 +0200 Subject: [PATCH 2/4] Replace `RotaryNavConfig` with `RotaryInputAdapter` (#204) Replaced the RotaryNavConfig struct with a new RotaryInputAdapter class to handle rotary input for LCD menus efficiently. The adapter manages user input through a rotary encoder, recognizing short, long, and double presses for menu navigation and selection. --- examples/InputRotary/InputRotary.ino | 18 ++----- examples/SimpleRotary/SimpleRotary.ino | 18 ++----- src/input/RotaryInputAdapter.h | 72 ++++++++++++++++++++++++++ src/utils/RotaryNavConfig.h | 65 ----------------------- 4 files changed, 82 insertions(+), 91 deletions(-) create mode 100644 src/input/RotaryInputAdapter.h delete mode 100644 src/utils/RotaryNavConfig.h diff --git a/examples/InputRotary/InputRotary.ino b/examples/InputRotary/InputRotary.ino index 01450098..15244e7d 100644 --- a/examples/InputRotary/InputRotary.ino +++ b/examples/InputRotary/InputRotary.ino @@ -4,8 +4,8 @@ #include #include #include +#include #include -#include #define LCD_ROWS 2 #define LCD_COLS 16 @@ -31,26 +31,18 @@ SUB_MENU( ITEM_INPUT_CHARSET("User", charset, inputCallback), ITEM_COMMAND("Clear", clearInput)); -LiquidCrystalI2CAdapter lcdAdapter(0x27, LCD_COLS, LCD_ROWS); -LcdMenu menu(lcdAdapter); - SimpleRotary encoder(2, 3, 4); -RotaryNavConfig menuConfig = { - .encoder = &encoder, - .menu = &menu, - .longPressDuration = 1000, -}; +LiquidCrystalI2CAdapter lcdAdapter(0x27, LCD_COLS, LCD_ROWS); +LcdMenu menu(lcdAdapter); +RotaryInputAdapter rotaryInput(&menu, &encoder); void setup() { Serial.begin(9600); menu.initialize(mainMenu); } -void loop() { - // Call the handleRotaryMenu function, passing the menuConfig instance - processWithRotaryEncoder(&menuConfig); -} +void loop() { rotaryInput.observe(); } // Define the callbacks void inputCallback(char* value) { diff --git a/examples/SimpleRotary/SimpleRotary.ino b/examples/SimpleRotary/SimpleRotary.ino index 806d82cf..34e5812f 100644 --- a/examples/SimpleRotary/SimpleRotary.ino +++ b/examples/SimpleRotary/SimpleRotary.ino @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -32,27 +33,18 @@ MAIN_MENU( ITEM_TOGGLE("Backlight", toggleBacklight), ITEM_BASIC("Blink random")); -LiquidCrystalI2CAdapter lcdAdapter(0x27, LCD_COLS, LCD_ROWS); -LcdMenu menu(lcdAdapter); - SimpleRotary encoder(2, 3, 4); -RotaryNavConfig menuConfig = { - .encoder = &encoder, - .menu = &menu, - .longPressDuration = 1000, - .doublePressThreshold = 500, -}; +LiquidCrystalI2CAdapter lcdAdapter(0x27, LCD_COLS, LCD_ROWS); +LcdMenu menu(lcdAdapter); +RotaryInputAdapter rotaryInput(&menu, &encoder); void setup() { Serial.begin(9600); menu.initialize(mainMenu); } -void loop() { - // Call the handleRotaryMenu function, passing the menuConfig instance - processWithRotaryEncoder(&menuConfig); -} +void loop() { rotaryInput.observe(); } // Define the callbacks void toggleBacklight(uint16_t isOn) { diff --git a/src/input/RotaryInputAdapter.h b/src/input/RotaryInputAdapter.h new file mode 100644 index 00000000..7f8a2d98 --- /dev/null +++ b/src/input/RotaryInputAdapter.h @@ -0,0 +1,72 @@ +#pragma once + +#include "InputInterface.h" +#include + +class RotaryInputAdapter : public InputInterface { + private: + uint16_t longPressDuration; // Duration to consider a long press + uint16_t doublePressThreshold; // Duration to consider a double press + unsigned long lastPressTime = 0; // Last time the button was pressed + bool pendingEnter = false; // Flag to indicate if an enter action is pending + + public: + SimpleRotary* encoder; + + /** + * @brief Adapter for handling rotary input for an LCD menu. + * + * This class interfaces with a rotary encoder to manage user input + * for navigating and selecting options in an LCD menu. + * + * @param menu Pointer to the LcdMenu instance that this adapter will control. + * @param encoder Pointer to the SimpleRotary instance representing the rotary encoder. + * @param longPressDuration Duration in milliseconds to recognize a long press (default is 1000 ms). + * @param doublePressThreshold Duration in milliseconds to recognize a double press (default is 300 ms). + */ + RotaryInputAdapter( + LcdMenu* menu, + SimpleRotary* encoder, + uint16_t longPressDuration = 1000, + uint16_t doublePressThreshold = 300) : InputInterface(menu), + encoder(encoder), + longPressDuration(longPressDuration), + doublePressThreshold(doublePressThreshold) {}; + + void observe() override { + // Handle rotary encoder rotation + uint8_t rotation = encoder->rotate(); + if (rotation == 1) { + menu->process(DOWN); // Call DOWN action + } else if (rotation == 2) { + menu->process(UP); // Call UP action + } + + // Handle button press (short, long, and double press) + uint8_t pressType = encoder->pushType(longPressDuration); + unsigned long currentTime = millis(); + + if (pressType == 1) { + if (pendingEnter) { + if (doublePressThreshold > 0 && + currentTime - lastPressTime < doublePressThreshold) { + menu->process(BACKSPACE); // Call BACKSPACE action (double press) + pendingEnter = false; + } + } else { + pendingEnter = true; + lastPressTime = currentTime; + } + } else if (pressType == 2) { + menu->process(BACK); // Call BACK action (long press) + pendingEnter = false; + } + + // Check if the doublePressThreshold has elapsed for pending enter action + if ((!menu->lcd.getEditModeEnabled() && pendingEnter) || + (pendingEnter && (currentTime - lastPressTime >= doublePressThreshold))) { + menu->process(ENTER); // Call ENTER action (short press) + pendingEnter = false; + } + } +}; \ No newline at end of file diff --git a/src/utils/RotaryNavConfig.h b/src/utils/RotaryNavConfig.h deleted file mode 100644 index 4d01aa9b..00000000 --- a/src/utils/RotaryNavConfig.h +++ /dev/null @@ -1,65 +0,0 @@ -#pragma once - -#include -#include - -/** - * @brief Configuration for rotary encoder navigation in the LCD menu. - * - * @param encoder Pointer to the rotary encoder instance - * @param menu Pointer to the LCD menu instance - * @param doublePressThreshold Duration (ms) to consider a double press - * @param longPressDuration Duration (ms) to consider a long press - * @param lastPressTime The last time the button was pressed - * @param pendingEnter Flag to indicate if an enter action is pending - */ -struct RotaryNavConfig { - SimpleRotary* encoder; - LcdMenu* menu; - uint16_t longPressDuration; - uint16_t doublePressThreshold; - unsigned long lastPressTime; - bool pendingEnter; -}; - -/** - * @brief Handles rotary encoder navigation in the LCD menu. - * - * @param config Pointer to the RotaryNavConfig struct - */ -void processWithRotaryEncoder(RotaryNavConfig* config) { - // Handle rotary encoder rotation - uint8_t rotation = config->encoder->rotate(); - if (rotation == 1) { - config->menu->process(DOWN); // Call DOWN action - } else if (rotation == 2) { - config->menu->process(UP); // Call UP action - } - - // Handle button press (short, long, and double press) - uint8_t pressType = config->encoder->pushType(config->longPressDuration); - unsigned long currentTime = millis(); - - if (pressType == 1) { - if (config->pendingEnter) { - if (config->doublePressThreshold > 0 && - currentTime - config->lastPressTime < config->doublePressThreshold) { - config->menu->process(BACKSPACE); // Call BACKSPACE action (double press) - config->pendingEnter = false; - } - } else { - config->pendingEnter = true; - config->lastPressTime = currentTime; - } - } else if (pressType == 2) { - config->menu->process(BACK); // Call BACK action (long press) - config->pendingEnter = false; - } - - // Check if the doublePressThreshold has elapsed for pending enter action - if ((!config->menu->lcd.getEditModeEnabled() && config->pendingEnter) || - (config->pendingEnter && (currentTime - config->lastPressTime >= config->doublePressThreshold))) { - config->menu->process(ENTER); // Call ENTER action (short press) - config->pendingEnter = false; - } -} \ No newline at end of file From bd65b59b8eb8a9e8429366389df0559aec9ceec7 Mon Sep 17 00:00:00 2001 From: Thomas Forntoh Date: Wed, 18 Sep 2024 00:19:51 +0200 Subject: [PATCH 3/4] Revert "Replace `RotaryNavConfig` with `RotaryInputAdapter`" (#205) Revert "Replace `RotaryNavConfig` with `RotaryInputAdapter` (#204)" This reverts commit 78b85eb8d5e3174346e3bfcbf741450edb78f121. --- examples/InputRotary/InputRotary.ino | 18 +++++-- examples/SimpleRotary/SimpleRotary.ino | 18 +++++-- src/input/RotaryInputAdapter.h | 72 -------------------------- src/utils/RotaryNavConfig.h | 65 +++++++++++++++++++++++ 4 files changed, 91 insertions(+), 82 deletions(-) delete mode 100644 src/input/RotaryInputAdapter.h create mode 100644 src/utils/RotaryNavConfig.h diff --git a/examples/InputRotary/InputRotary.ino b/examples/InputRotary/InputRotary.ino index 15244e7d..01450098 100644 --- a/examples/InputRotary/InputRotary.ino +++ b/examples/InputRotary/InputRotary.ino @@ -4,8 +4,8 @@ #include #include #include -#include #include +#include #define LCD_ROWS 2 #define LCD_COLS 16 @@ -31,18 +31,26 @@ SUB_MENU( ITEM_INPUT_CHARSET("User", charset, inputCallback), ITEM_COMMAND("Clear", clearInput)); -SimpleRotary encoder(2, 3, 4); - LiquidCrystalI2CAdapter lcdAdapter(0x27, LCD_COLS, LCD_ROWS); LcdMenu menu(lcdAdapter); -RotaryInputAdapter rotaryInput(&menu, &encoder); + +SimpleRotary encoder(2, 3, 4); + +RotaryNavConfig menuConfig = { + .encoder = &encoder, + .menu = &menu, + .longPressDuration = 1000, +}; void setup() { Serial.begin(9600); menu.initialize(mainMenu); } -void loop() { rotaryInput.observe(); } +void loop() { + // Call the handleRotaryMenu function, passing the menuConfig instance + processWithRotaryEncoder(&menuConfig); +} // Define the callbacks void inputCallback(char* value) { diff --git a/examples/SimpleRotary/SimpleRotary.ino b/examples/SimpleRotary/SimpleRotary.ino index 34e5812f..806d82cf 100644 --- a/examples/SimpleRotary/SimpleRotary.ino +++ b/examples/SimpleRotary/SimpleRotary.ino @@ -3,7 +3,6 @@ #include #include #include -#include #include #include @@ -33,18 +32,27 @@ MAIN_MENU( ITEM_TOGGLE("Backlight", toggleBacklight), ITEM_BASIC("Blink random")); -SimpleRotary encoder(2, 3, 4); - LiquidCrystalI2CAdapter lcdAdapter(0x27, LCD_COLS, LCD_ROWS); LcdMenu menu(lcdAdapter); -RotaryInputAdapter rotaryInput(&menu, &encoder); + +SimpleRotary encoder(2, 3, 4); + +RotaryNavConfig menuConfig = { + .encoder = &encoder, + .menu = &menu, + .longPressDuration = 1000, + .doublePressThreshold = 500, +}; void setup() { Serial.begin(9600); menu.initialize(mainMenu); } -void loop() { rotaryInput.observe(); } +void loop() { + // Call the handleRotaryMenu function, passing the menuConfig instance + processWithRotaryEncoder(&menuConfig); +} // Define the callbacks void toggleBacklight(uint16_t isOn) { diff --git a/src/input/RotaryInputAdapter.h b/src/input/RotaryInputAdapter.h deleted file mode 100644 index 7f8a2d98..00000000 --- a/src/input/RotaryInputAdapter.h +++ /dev/null @@ -1,72 +0,0 @@ -#pragma once - -#include "InputInterface.h" -#include - -class RotaryInputAdapter : public InputInterface { - private: - uint16_t longPressDuration; // Duration to consider a long press - uint16_t doublePressThreshold; // Duration to consider a double press - unsigned long lastPressTime = 0; // Last time the button was pressed - bool pendingEnter = false; // Flag to indicate if an enter action is pending - - public: - SimpleRotary* encoder; - - /** - * @brief Adapter for handling rotary input for an LCD menu. - * - * This class interfaces with a rotary encoder to manage user input - * for navigating and selecting options in an LCD menu. - * - * @param menu Pointer to the LcdMenu instance that this adapter will control. - * @param encoder Pointer to the SimpleRotary instance representing the rotary encoder. - * @param longPressDuration Duration in milliseconds to recognize a long press (default is 1000 ms). - * @param doublePressThreshold Duration in milliseconds to recognize a double press (default is 300 ms). - */ - RotaryInputAdapter( - LcdMenu* menu, - SimpleRotary* encoder, - uint16_t longPressDuration = 1000, - uint16_t doublePressThreshold = 300) : InputInterface(menu), - encoder(encoder), - longPressDuration(longPressDuration), - doublePressThreshold(doublePressThreshold) {}; - - void observe() override { - // Handle rotary encoder rotation - uint8_t rotation = encoder->rotate(); - if (rotation == 1) { - menu->process(DOWN); // Call DOWN action - } else if (rotation == 2) { - menu->process(UP); // Call UP action - } - - // Handle button press (short, long, and double press) - uint8_t pressType = encoder->pushType(longPressDuration); - unsigned long currentTime = millis(); - - if (pressType == 1) { - if (pendingEnter) { - if (doublePressThreshold > 0 && - currentTime - lastPressTime < doublePressThreshold) { - menu->process(BACKSPACE); // Call BACKSPACE action (double press) - pendingEnter = false; - } - } else { - pendingEnter = true; - lastPressTime = currentTime; - } - } else if (pressType == 2) { - menu->process(BACK); // Call BACK action (long press) - pendingEnter = false; - } - - // Check if the doublePressThreshold has elapsed for pending enter action - if ((!menu->lcd.getEditModeEnabled() && pendingEnter) || - (pendingEnter && (currentTime - lastPressTime >= doublePressThreshold))) { - menu->process(ENTER); // Call ENTER action (short press) - pendingEnter = false; - } - } -}; \ No newline at end of file diff --git a/src/utils/RotaryNavConfig.h b/src/utils/RotaryNavConfig.h new file mode 100644 index 00000000..4d01aa9b --- /dev/null +++ b/src/utils/RotaryNavConfig.h @@ -0,0 +1,65 @@ +#pragma once + +#include +#include + +/** + * @brief Configuration for rotary encoder navigation in the LCD menu. + * + * @param encoder Pointer to the rotary encoder instance + * @param menu Pointer to the LCD menu instance + * @param doublePressThreshold Duration (ms) to consider a double press + * @param longPressDuration Duration (ms) to consider a long press + * @param lastPressTime The last time the button was pressed + * @param pendingEnter Flag to indicate if an enter action is pending + */ +struct RotaryNavConfig { + SimpleRotary* encoder; + LcdMenu* menu; + uint16_t longPressDuration; + uint16_t doublePressThreshold; + unsigned long lastPressTime; + bool pendingEnter; +}; + +/** + * @brief Handles rotary encoder navigation in the LCD menu. + * + * @param config Pointer to the RotaryNavConfig struct + */ +void processWithRotaryEncoder(RotaryNavConfig* config) { + // Handle rotary encoder rotation + uint8_t rotation = config->encoder->rotate(); + if (rotation == 1) { + config->menu->process(DOWN); // Call DOWN action + } else if (rotation == 2) { + config->menu->process(UP); // Call UP action + } + + // Handle button press (short, long, and double press) + uint8_t pressType = config->encoder->pushType(config->longPressDuration); + unsigned long currentTime = millis(); + + if (pressType == 1) { + if (config->pendingEnter) { + if (config->doublePressThreshold > 0 && + currentTime - config->lastPressTime < config->doublePressThreshold) { + config->menu->process(BACKSPACE); // Call BACKSPACE action (double press) + config->pendingEnter = false; + } + } else { + config->pendingEnter = true; + config->lastPressTime = currentTime; + } + } else if (pressType == 2) { + config->menu->process(BACK); // Call BACK action (long press) + config->pendingEnter = false; + } + + // Check if the doublePressThreshold has elapsed for pending enter action + if ((!config->menu->lcd.getEditModeEnabled() && config->pendingEnter) || + (config->pendingEnter && (currentTime - config->lastPressTime >= config->doublePressThreshold))) { + config->menu->process(ENTER); // Call ENTER action (short press) + config->pendingEnter = false; + } +} \ No newline at end of file From 162efcbc73c2e4aa006df4290d152884410e8703 Mon Sep 17 00:00:00 2001 From: forntoh Date: Wed, 18 Sep 2024 01:27:01 +0200 Subject: [PATCH 4/4] Fix compilation issue --- .github/workflows/compile-arduino.yml | 2 +- src/LcdMenu.h | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/compile-arduino.yml b/.github/workflows/compile-arduino.yml index c3400bd8..e5d573c2 100644 --- a/.github/workflows/compile-arduino.yml +++ b/.github/workflows/compile-arduino.yml @@ -1,5 +1,5 @@ name: Compile Examples -on: [pull_request_target] +on: [pull_request_target, pull_request] jobs: compile: diff --git a/src/LcdMenu.h b/src/LcdMenu.h index 4736991b..74a9e6d7 100644 --- a/src/LcdMenu.h +++ b/src/LcdMenu.h @@ -84,17 +84,6 @@ class LcdMenu { lcd.drawCursor(); // In case if currentPosition was not changed between screens } - /* - * Draw the menu items and cursor - */ - void update() { - if (!enableUpdate) { - return; - } - drawMenu(); - lcd.drawCursor(); - } - /* * Draw the cursor */ @@ -153,6 +142,17 @@ class LcdMenu { lcd.drawCursor(); } + /* + * Draw the menu items and cursor + */ + void update() { + if (!enableUpdate) { + return; + } + drawMenu(); + lcd.drawCursor(); + } + bool process(const unsigned char c) { if (currentMenuTable[cursorPosition]->process(c)) { return true;