基于ESP32-S2与SH1106 OLED的三级菜单系统开发求助
三级菜单系统实现指引(基于ESP32-S2、SH1106 OLED与旋转编码器)
需求概述
- 硬件组成:ESP32-S2、SH1106 OLED屏、带按键旋转编码器
- 菜单结构:
Main Menu Sub Menu 1(Presets) Preset1 Preset2 Preset3 ...(数量可动态调整) Sub Menu 2(Precise) Number Selector - 交互逻辑:旋转编码器切换菜单选项,短按进入子菜单,长按返回上级菜单
现有基础
已基于OLEDDisplayUi库实现主菜单的两帧切换、旋转编码器的帧导航、按键长短按检测功能,现有核心代码如下:
#include "SPI.h" #include "Wire.h" #include "SH1106Wire.h" #include "AiEsp32RotaryEncoder.h" #include "OLEDDisplayUi.h" #include "images.h" #define ROTARY_ENCODER_A_PIN 7 #define ROTARY_ENCODER_B_PIN 5 #define ROTARY_ENCODER_BUTTON_PIN 3 #define ROTARY_ENCODER_STEPS 4 AiEsp32RotaryEncoder rotaryEncoder = AiEsp32RotaryEncoder(ROTARY_ENCODER_A_PIN, ROTARY_ENCODER_B_PIN, ROTARY_ENCODER_BUTTON_PIN, -1, ROTARY_ENCODER_STEPS); SH1106Wire display(0x3c, 8, 9); OLEDDisplayUi ui ( &display ); unsigned long shortPressAfterMiliseconds = 50; unsigned long longPressAfterMiliseconds = 600; void shortPress() { Serial.println("Button SHORT press"); Serial.println(ui.getUiState()->frameState); } void longPress() { Serial.println("Button LONG press"); } void IRAM_ATTR readEncoderISR() { rotaryEncoder.readEncoder_ISR(); } void encoderButton() { static unsigned long lastTimeButtonDown = 0; static bool wasButtonDown = false; static bool wasLong = false; bool isEncoderButtonDown = rotaryEncoder.isEncoderButtonDown(); if ( isEncoderButtonDown ) { if ( !wasButtonDown ) { lastTimeButtonDown = millis(); wasButtonDown = true; } if ( !wasLong ) { if ( millis() - lastTimeButtonDown >= longPressAfterMiliseconds ) { longPress(); wasLong = true; } } return; } if ( wasButtonDown && !wasLong ) { if ( millis() - lastTimeButtonDown >= shortPressAfterMiliseconds ) { shortPress(); wasLong = false; } } wasButtonDown = false; wasLong = false; } void encoderLoop() { if (rotaryEncoder.encoderChanged()) { static int8_t prev = 0; int8_t curr = rotaryEncoder.readEncoder(); if ( curr > prev ) { ui.nextFrame(); } else if ( curr < prev ) { ui.previousFrame(); } prev = rotaryEncoder.readEncoder(); } encoderButton(); } void frame1(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(ArialMT_Plain_24); display->drawString(64 + x, 19 + y, "Presets"); } void frame2(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(ArialMT_Plain_24); display->drawString(64 + x, 19 + y, "Precise"); } FrameCallback frames[] = { frame1, frame2 }; int frameCount = 2; void setupMainMenu() { ui.setTargetFPS(60); ui.setActiveSymbol(activeSymbol); ui.setInactiveSymbol(inactiveSymbol); ui.setIndicatorPosition(TOP); ui.setIndicatorDirection(LEFT_RIGHT); ui.setFrameAnimation(SLIDE_LEFT); ui.setFrames(frames, frameCount); ui.disableAutoTransition(); ui.setTimePerTransition(60); ui.init(); } void setup() { Serial.begin(115200); Serial.println(); Serial.println(); pinMode(ROTARY_ENCODER_A_PIN, INPUT_PULLUP); pinMode(ROTARY_ENCODER_B_PIN, INPUT_PULLUP); rotaryEncoder.begin(); rotaryEncoder.setup(readEncoderISR); rotaryEncoder.setAcceleration(80); setupMainMenu(); display.flipScreenVertically(); } void loop() { encoderLoop(); ui.update(); }
三级菜单实现方案
1. 菜单状态管理
添加全局变量跟踪当前菜单层级与选中项:
// 菜单层级枚举 enum MenuLevel { MAIN_MENU, SUB_MENU_PRESETS, SUB_MENU_PRECISE }; MenuLevel currentLevel = MAIN_MENU; int currentSelection = 0; // 当前选中的选项索引
2. 按键交互逻辑修改
更新长短按函数,实现菜单层级跳转:
void shortPress() { switch(currentLevel) { case MAIN_MENU: // 进入对应子菜单 if(currentSelection == 0) { currentLevel = SUB_MENU_PRESETS; currentSelection = 0; setupPresetsMenu(); } else if(currentSelection == 1) { currentLevel = SUB_MENU_PRECISE; currentSelection = 0; setupPreciseMenu(); } break; case SUB_MENU_PRESETS: // 处理预设选中逻辑(示例:打印选中项) Serial.print("Selected Preset: "); Serial.println(currentSelection + 1); break; case SUB_MENU_PRECISE: // 处理数字选择器确认逻辑 break; } } void longPress() { // 非主菜单时返回上级 if(currentLevel != MAIN_MENU) { currentLevel = MAIN_MENU; currentSelection = 0; setupMainMenu(); } }
3. 编码器旋转逻辑适配
修改编码器处理函数,根据当前菜单层级切换选项:
void encoderLoop() { if (rotaryEncoder.encoderChanged()) { static int8_t prev = 0; int8_t curr = rotaryEncoder.readEncoder(); int maxSelection = 0; // 根据菜单层级确定最大可选索引 switch(currentLevel) { case MAIN_MENU: maxSelection = frameCount - 1; break; case SUB_MENU_PRESETS: maxSelection = presetCount - 1; break; case SUB_MENU_PRECISE: maxSelection = 99; // 数字选择器范围设为0-99 break; } if (curr > prev) { currentSelection = (currentSelection + 1) % (maxSelection + 1); currentLevel == MAIN_MENU ? ui.nextFrame() : ui.update(); } else if (curr < prev) { currentSelection = (currentSelection - 1 + maxSelection + 1) % (maxSelection + 1); currentLevel == MAIN_MENU ? ui.previousFrame() : ui.update(); } prev = curr; } encoderButton(); }
4. 子菜单模块化实现
Presets子菜单(支持动态数量)
// 预设名称数组,可直接修改数量 const char* presets[] = {"Preset1", "Preset2", "Preset3", "Preset4"}; int presetCount = sizeof(presets)/sizeof(presets[0]); // Presets子菜单帧绘制 void presetFrame(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(ArialMT_Plain_16); display->drawString(64 + x, 10 + y, "Presets"); display->setFont(ArialMT_Plain_24); display->drawString(64 + x, 30 + y, presets[currentSelection]); } FrameCallback presetFrames[] = {presetFrame}; int presetFrameCount = 1; // Presets子菜单初始化 void setupPresetsMenu() { ui.resetUiState(); ui.setFrames(presetFrames, presetFrameCount); ui.setIndicatorPosition(NONE); // 子菜单隐藏顶部指示器 ui.setFrameAnimation(SLIDE_UP); ui.init(); }
Precise子菜单(数字选择器)
int selectedNumber = 0; // 数字选择器帧绘制 void preciseFrame(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(ArialMT_Plain_16); display->drawString(64 + x, 10 + y, "Number Selector"); display->setFont(ArialMT_Plain_24); char numStr[4]; sprintf(numStr, "%d", selectedNumber); display->drawString(64 + x, 30 + y, numStr); } FrameCallback preciseFrames[] = {preciseFrame}; int preciseFrameCount = 1; // 数字选择器菜单初始化 void setupPreciseMenu() { ui.resetUiState(); ui.setFrames(preciseFrames, preciseFrameCount); ui.setIndicatorPosition(NONE); ui.setFrameAnimation(SLIDE_UP); ui.init(); }
5. 模块化重构建议
- 将菜单逻辑封装为
Menu类,每个菜单层级对应一个类实例,包含状态管理、帧绘制、初始化方法 - 预设列表可改为动态数组(如
std::vector),支持运行时添加/删除预设 - 把菜单配置(如字体、动画、指示器位置)抽离为单独的配置函数,统一管理样式
内容的提问来源于stack exchange,提问作者Mar
相关产品推荐
相关产品推荐

