You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 14:47:12