extern "C"{ #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/queue.h" #include "esp_log.h" #include "ssd1306.h" } #include "menu.hpp" #include "encoder.hpp" #include "config.hpp" #include "motorctl.hpp" //--- variables --- static const char *TAG = "menu"; static menuState_t menuState = MAIN_MENU; static int value = 0; //================================ //===== CONFIGURE MENU ITEMS ===== //================================ // note: when line4 * and line5 * are empty the value is printed large //######################### //#### center Joystick #### //######################### void item_centerJoystick_action(int value, SSD1306_t * display){ if (!value) return; ESP_LOGW(TAG, "defining joystick center"); joystick.defineCenter(); } int item_centerJoystick_value(){ return 1; } menuItem_t item_centerJoystick = { item_centerJoystick_action, // function action item_centerJoystick_value, 0, // valueMin 1, // valueMAx 1, // valueIncrement "Center Joystick", // title "Center Joystick", // line1 (above value) "click to confirm", // line2 (above value) "defines current", // line4 * (below value) "pos as center", // line5 * "click to confirm", // line6 "set 0 to cancel", // line7 }; //######################## //#### debug Joystick #### //######################## //continously show/update joystick data on display void item_debugJoystick_action(int value, SSD1306_t * display) { //--- variables --- bool running = true; rotary_encoder_event_t event; //-- pre loop instructions -- if (!value) // dont open menu when value was set to 0 return; ESP_LOGW(TAG, "showing joystick debug page"); ssd1306_clear_screen(display, false); // show title displayTextLine(display, 0, false, true, " - debug stick - "); // show info line displayTextLineCentered(display, 7, false, true, "click to exit"); //-- show/update values -- // stop when button pressed or control state changes (timeouts to IDLE) while (running && control.getCurrentMode() == controlMode_t::MENU) { // repeatedly print all joystick data joystickData_t data = joystick.getData(); displayTextLine(display, 1, false, false, "x = %.3f ", data.x); displayTextLine(display, 2, false, false, "y = %.3f ", data.y); displayTextLine(display, 3, false, false, "radius = %.3f", data.radius); displayTextLine(display, 4, false, false, "angle = %-06.3f ", data.angle); displayTextLine(display, 5, false, false, "pos=%-12s ", joystickPosStr[(int)data.position]); // exit when button pressed if (xQueueReceive(encoderQueue, &event, 20 / portTICK_PERIOD_MS)) { switch (event.type) { case RE_ET_BTN_CLICKED: running = false; break; case RE_ET_CHANGED: case RE_ET_BTN_PRESSED: case RE_ET_BTN_RELEASED: case RE_ET_BTN_LONG_PRESSED: break; } } } } int item_debugJoystick_value(){ return 1; } menuItem_t item_debugJoystick = { item_debugJoystick_action, // function action item_debugJoystick_value, 0, // valueMin 1, // valueMAx 1, // valueIncrement "Debug joystick", // title "Debug joystick", // line1 (above value) "", // line2 (above value) "click to enter", // line4 * (below value) "debug screen", // line5 * "prints values", // line6 "set 0 to cancel", // line7 }; //######################## //##### set max duty ##### //######################## void maxDuty_action(int value, SSD1306_t * display) { //TODO actually store the value ESP_LOGW(TAG, "set max duty to %d", value); } int maxDuty_currentValue() { //TODO get real current value return 84; } menuItem_t item_maxDuty = { maxDuty_action, // function action maxDuty_currentValue, 1, // valueMin 99, // valueMAx 1, // valueIncrement "max duty", // title "", // line1 (above value) " set max-duty: ", // line2 (above value) "", // line4 * (below value) "", // line5 * " 1-99 ", // line6 " percent ", // line7 }; //###################### //##### accelLimit ##### //###################### void item_accelLimit_action(int value, SSD1306_t * display) { motorLeft.setFade(fadeType_t::ACCEL, (uint32_t)value); motorRight.setFade(fadeType_t::ACCEL, (uint32_t)value); } int item_accelLimit_value() { return motorLeft.getFade(fadeType_t::ACCEL); } menuItem_t item_accelLimit = { item_accelLimit_action, // function action item_accelLimit_value, 0, // valueMin 10000, // valueMAx 100, // valueIncrement "Accel limit", // title "Accel limit /", // line1 (above value) "Fade up time", // line2 (above value) "", // line4 * (below value) "", // line5 * "milliseconds", // line6 "from 0 to 100%", // line7 }; // ###################### // ##### decelLimit ##### // ###################### void item_decelLimit_action(int value, SSD1306_t * display) { motorLeft.setFade(fadeType_t::DECEL, (uint32_t)value); motorRight.setFade(fadeType_t::DECEL, (uint32_t)value); } int item_decelLimit_value() { return motorLeft.getFade(fadeType_t::DECEL); } menuItem_t item_decelLimit = { item_decelLimit_action, // function action item_decelLimit_value, 0, // valueMin 10000, // valueMAx 100, // valueIncrement "Decel limit", // title "Decel limit /", // line1 (above value) "Fade down time", // line2 (above value) "", // line4 * (below value) "", // line5 * "milliseconds", // line6 "from 100 to 0%", // line7 }; //##################### //###### example ###### //##################### void item_example_action(int value, SSD1306_t * display) { return; } int item_example_value(){ return 53; } menuItem_t item_example = { item_example_action, // function action item_example_value, -255, // valueMin 255, // valueMAx 2, // valueIncrement "example-item-max", // title "line 1 - above", // line1 (above value) "line 2 - above", // line2 (above value) "line 4 - below", // line4 * (below value) "line 5 - below", // line5 * "line 6 - below", // line6 "line 7 - last", // line7 }; menuItem_t item_last = { item_example_action, // function action item_example_value, -500, // valueMin 4500, // valueMAx 50, // valueIncrement "set large number", // title "line 1 - above", // line1 (above value) "line 2 - above", // line2 (above value) "", // line4 * (below value) "", // line5 * "line 6 - below", // line6 "line 7 - last", // line7 }; //store all configured menu items in one array menuItem_t menuItems[] = {item_centerJoystick, item_debugJoystick, item_accelLimit, item_decelLimit, item_example, item_last}; int itemCount = 6; //-------------------------- //------ showItemList ------ //-------------------------- //function that renders main menu to display (one update) //list of all menu items with currently selected highlighted #define SELECTED_ITEM_LINE 4 #define FIRST_ITEM_LINE 1 #define LAST_ITEM_LINE 7 void showItemList(SSD1306_t *display, int selectedItem) { //-- show title line -- displayTextLine(display, 0, false, true, " --- menu --- "); //inverted //-- show item list -- for (int line = FIRST_ITEM_LINE; line <= LAST_ITEM_LINE; line++) { // loop through all lines int printItemIndex = selectedItem - SELECTED_ITEM_LINE + line; // TODO: when reaching end of items, instead of showing "empty" change position of selected item to still use the entire screen if (printItemIndex < 0 || printItemIndex >= itemCount) // out of range of available items { // no item in this line displayTextLine(display, line, false, false, " -- empty -- "); } else { if (printItemIndex == selectedItem) { // selected item -> add '> ' and print inverted displayTextLine(display, line, false, true, "> %-14s", menuItems[printItemIndex].title); // inverted } else { // not the selected item -> print normal displayTextLine(display, line, false, false, "%-16s", menuItems[printItemIndex].title); } } // logging ESP_LOGD(TAG, "showItemList: loop - line=%d, item=%d, (selected=%d '%s')", line, printItemIndex, selectedItem, menuItems[selectedItem].title); } } //--------------------------- //----- showValueSelect ----- //--------------------------- // function that renders value-select screen to display (one update) // shows configured text of selected item and currently selected value // TODO show previous value in one line? // TODO update changed line only (value) void showValueSelect(SSD1306_t *display, int selectedItem) { //-- show title line -- displayTextLine(display, 0, false, true, " -- set value -- "); // inverted //-- show text above value -- displayTextLine(display, 1, false, false, "%-16s", menuItems[selectedItem].line1); displayTextLine(display, 2, false, false, "%-16s", menuItems[selectedItem].line2); //-- show value and other configured lines -- // print value large, if 2 description lines are empty if (strlen(menuItems[selectedItem].line4) == 0 && strlen(menuItems[selectedItem].line5) == 0) { // print large value + line5 and line6 displayTextLineCentered(display, 3, true, false, "%d", value); //large centered displayTextLine(display, 6, false, false, "%-16s", menuItems[selectedItem].line6); displayTextLine(display, 7, false, false, "%-16s", menuItems[selectedItem].line7); } else { displayTextLineCentered(display, 3, false, false, "%d", value); //centered // print description lines 4 to 7 displayTextLine(display, 4, false, false, "%-16s", menuItems[selectedItem].line4); displayTextLine(display, 5, false, false, "%-16s", menuItems[selectedItem].line5); displayTextLine(display, 6, false, false, "%-16s", menuItems[selectedItem].line6); displayTextLine(display, 7, false, false, "%-16s", menuItems[selectedItem].line7); } } //======================== //====== handleMenu ====== //======================== //controls menu with encoder input and displays the text on oled display //function is repeatedly called by display task when in menu state #define QUEUE_TIMEOUT 3000 //timeout no encoder event - to handle timeout and not block the display loop #define MENU_TIMEOUT 60000 //inactivity timeout (switch to IDLE mode) void handleMenu(SSD1306_t *display) { static uint32_t lastActivity = 0; static int selectedItem = 0; rotary_encoder_event_t event; // store event data //--- handle different menu states --- switch (menuState) { //------------------------- //---- State MAIN MENU ---- //------------------------- case MAIN_MENU: // update display showItemList(display, selectedItem); // shows list of items with currently selected one on display // wait for encoder event if (xQueueReceive(encoderQueue, &event, QUEUE_TIMEOUT / portTICK_PERIOD_MS)) { lastActivity = esp_log_timestamp(); switch (event.type) { case RE_ET_CHANGED: //--- scroll in list --- if (event.diff < 0) { if (selectedItem != itemCount - 1) selectedItem++; ESP_LOGD(TAG, "showing next item: %d '%s'", selectedItem, menuItems[selectedItem].title); //note: display will update at start of next run } else { if (selectedItem != 0) selectedItem--; ESP_LOGD(TAG, "showing previous item: %d '%s'", selectedItem, menuItems[selectedItem].title); //note: display will update at start of next run } break; case RE_ET_BTN_CLICKED: //--- switch to edit value page --- ESP_LOGI(TAG, "Button pressed - switching to state SET_VALUE"); // change state (menu to set value) menuState = SET_VALUE; // get currently configured value value = menuItems[selectedItem].currentValue(); // clear display ssd1306_clear_screen(display, false); break; //exit menu mode case RE_ET_BTN_LONG_PRESSED: //change to previous mode (e.g. JOYSTICK) control.toggleMode(controlMode_t::MENU); //currently already in MENU -> changes to previous mode ssd1306_clear_screen(display, false); break; case RE_ET_BTN_RELEASED: case RE_ET_BTN_PRESSED: break; } } break; //------------------------- //---- State SET VALUE ---- //------------------------- case SET_VALUE: // wait for encoder event showValueSelect(display, selectedItem); if (xQueueReceive(encoderQueue, &event, QUEUE_TIMEOUT / portTICK_PERIOD_MS)) { lastActivity = esp_log_timestamp(); switch (event.type) { case RE_ET_CHANGED: //-- change value -- // increment value if (event.diff < 0) value += menuItems[selectedItem].valueIncrement; else value -= menuItems[selectedItem].valueIncrement; // limit to min/max range if (value > menuItems[selectedItem].valueMax) value = menuItems[selectedItem].valueMax; if (value < menuItems[selectedItem].valueMin) value = menuItems[selectedItem].valueMin; break; case RE_ET_BTN_CLICKED: //-- apply value -- ESP_LOGI(TAG, "Button pressed - running action function with value=%d for item '%s'", value, menuItems[selectedItem].title); menuItems[selectedItem].action(value, display); menuState = MAIN_MENU; break; case RE_ET_BTN_PRESSED: case RE_ET_BTN_RELEASED: case RE_ET_BTN_LONG_PRESSED: break; } } break; } //-------------------- //--- menu timeout --- //-------------------- //close menu and switch to IDLE mode when no encoder event within MENU_TIMEOUT if (esp_log_timestamp() - lastActivity > MENU_TIMEOUT) { ESP_LOGW(TAG, "TIMEOUT - no activity for more than %ds -> closing menu, switching to IDLE", MENU_TIMEOUT/1000); // reset menu selectedItem = 0; menuState = MAIN_MENU; ssd1306_clear_screen(display, false); // change control mode control.changeMode(controlMode_t::IDLE); return; } }