U8g2图形库使用技巧记录(4)

        ~最近在优化平衡车的UI界面~

        寻寻觅觅间,偶然在B站上看到一个很优雅的UI框架WouoUI,底层的oled驱动操作使用的是U8G2方式,于是花了点时间把它移植了过来(丙正正),跑在小平衡车(小黑)上效果真的很哇塞,以一路foc电机+一个按钮作为输入;

~以下是UI的视频效果:

~以下是移植的源码,希望对想移植的后来者有所帮助:

/************************************* 屏幕驱动 *************************************/
//分辨率128*64,可以使用硬件IIC接口
#include "flex_ui.h"
#include "u8g2_ssd1306.h"

#include <driver/gpio.h>
#include <driver/spi_master.h>
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "nvs_flash.h"
#include "nvs.h"

#include "sbus.h"
#include "param.h"
#include "ms5611.h"
#include "tle5012b.h"
#include "drv_adc.h"
#include "drv_motor.h"
#include "stabilizer.h"
#include <stdio.h>
#include <string.h>
#include <u8g2.h>

#include "wiring.h"
#include "lv_math.h"

static const char *TAG = "flex_ui";

/************************************* 定义页面 *************************************/

//总目录,缩进表示页面层级
enum 
{
  M_WINDOW,
  M_SLEEP,
    M_MAIN, 
      M_EDITOR,
        M_KNOB,
          M_KRF,
          M_KPF,
      M_VOLT,
      M_SETTING,
        M_ABOUT,
};

//状态,初始化标签
enum
{
  S_FADE,       //转场动画
  S_WINDOW,     //弹窗初始化
  S_LAYER_IN,   //层级初始化
  S_LAYER_OUT,  //层级初始化
  S_NONE,       //直接选择页面
};

//菜单结构体
typedef struct MENU
{
  char *m_select;
} M_SELECT;

/************************************* 定义内容 *************************************/

/************************************* 文字内容 *************************************/

M_SELECT main_menu[]
{
  {"Sleep"},
  {"Editor"},
  {"Volt"},
  {"Setting"},
};

M_SELECT editor_menu[]
{
  {"[ Editor ]"},
  {"- Function 0"},
  {"- Function 1"},
  {"- Function 2"},
  {"- Function 3"},
  {"- Function 4"},
  {"- Function 5"},
  {"- Function 6"},
  {"- Function 7"},
  {"- Function 8"},
  {"- Function 9"},
  {"- Knob"},
};

M_SELECT knob_menu[]
{
  {"[ Knob ]"},
  {"# Rotate Func"},
  {"$ Press Func"},
};

M_SELECT krf_menu[]
{
  {"[ Rotate Function ]"},
  {"--------------------------"},
  {"= Disable"},
  {"--------------------------"},
  {"= Volume"},
  {"= Brightness"},
  {"--------------------------"},
};

M_SELECT kpf_menu[]
{
  {"[ Press Function ]"},
  {"--------------------------"},
  {"= Disable"},
  {"--------------------------"},
  {"= A"},
  {"= B"},
  {"= C"},
  {"= D"},
  {"= E"},
  {"= F"},
  {"= G"},
  {"= H"},
  {"= I"},
  {"= J"},
  {"= K"},
  {"= L"},
  {"= M"},
  {"= N"},
  {"= O"},
  {"= P"},
  {"= Q"},
  {"= R"},
  {"= S"},
  {"= T"},
  {"= U"},
  {"= V"},
  {"= W"},
  {"= X"},
  {"= Y"},
  {"= Z"},
  {"--------------------------"},
  {"= 0"},
  {"= 1"},
  {"= 2"},
  {"= 3"},
  {"= 4"},
  {"= 5"},
  {"= 6"},
  {"= 7"},
  {"= 8"},
  {"= 9"},
  {"--------------------------"},
  {"= Esc"},
  {"= F1"},
  {"= F2"},
  {"= F3"},
  {"= F4"},
  {"= F5"},
  {"= F6"},
  {"= F7"},
  {"= F8"},
  {"= F9"},
  {"= F10"},
  {"= F11"},
  {"= F12"},
  {"--------------------------"},
  {"= Left Ctrl"},
  {"= Left Shift"},
  {"= Left Alt"},
  {"= Left Win"},
  {"= Right Ctrl"},
  {"= Right Shift"},
  {"= Right Alt"},
  {"= Right Win"},
  {"--------------------------"},
  {"= Caps Lock"},
  {"= Backspace"},
  {"= Return"},
  {"= Insert"},
  {"= Delete"},
  {"= Tab"},
  {"--------------------------"},
  {"= Home"},
  {"= End"},
  {"= Page Up"},
  {"= Page Down"},
  {"--------------------------"},
  {"= Up Arrow"},
  {"= Down Arrow"},
  {"= Left Arrow"},
  {"= Right Arrow"},
  {"--------------------------"},
};

M_SELECT volt_menu[]
{
  {"A0"},
  {"A1"},
  {"A2"},
  {"A3"},
  {"A4"},
  {"A5"},
  {"A6"},
  {"A7"},
  {"B0"},
  {"B1"},
};

M_SELECT setting_menu[]
{
  {"[ Setting ]"},
  {"~ Disp Bri"},
  {"~ Tile Ani"},
  {"~ List Ani"},
  {"~ Win Ani"},
  {"~ Spot Ani"},
  {"~ Tag Ani"},
  {"~ Fade Ani"},
  {"~ Btn SPT"},
  {"~ Btn LPT"},
  {"+ T Ufd Fm Scr"},
  {"+ L Ufd Fm Scr"},
  {"+ T Loop Mode"},
  {"+ L Loop Mode"},
  {"+ Win Bokeh Bg"},
  {"+ Knob Rot Dir"},
  {"+ Dark Mode"},
  {"- [ About ]"},
};

M_SELECT about_menu[]
{
  {"[ FlexUI ]"},
  {"- Version: v1.0"},
  {"- Board: ESP32 PICO"},
  {"- Ram: 340k"},
  {"- Flash: 4MB"},
  {"- Freq: 240Mhz"},
  {"- Creator: Leon"},
  {"- Billi UID: 20230705"},  
};

/************************************* 图片内容 *************************************/

const uint8_t main_icon_pic[][120]
{
  {
    0xFF,0xFF,0xFF,0x3F,0xFF,0xFF,0xFF,0x3F,0xFF,0xFF,0xFF,0x3F,0xFF,0xFF,0xF1,0x3F,
    0xFF,0xFF,0xC3,0x3F,0xFF,0xFF,0x87,0x3F,0xFF,0xFF,0x07,0x3F,0xFF,0xFF,0x0F,0x3E,
    0xFF,0xFF,0x0F,0x3E,0xFF,0xFF,0x0F,0x3C,0xFF,0xFF,0x0F,0x3C,0xFF,0xFF,0x0F,0x38,
    0xFF,0xFF,0x0F,0x38,0xFF,0xFF,0x0F,0x38,0xFF,0xFF,0x07,0x38,0xFF,0xFF,0x07,0x38,
    0xFF,0xFF,0x03,0x38,0xF7,0xFF,0x01,0x38,0xE7,0xFF,0x00,0x3C,0x87,0x3F,0x00,0x3C,
    0x0F,0x00,0x00,0x3E,0x0F,0x00,0x00,0x3E,0x1F,0x00,0x00,0x3F,0x3F,0x00,0x80,0x3F,
    0x7F,0x00,0xC0,0x3F,0xFF,0x01,0xF0,0x3F,0xFF,0x07,0xFC,0x3F,0xFF,0xFF,0xFF,0x3F,
    0xFF,0xFF,0xFF,0x3F,0xFF,0xFF,0xFF,0x3F
  },
  {
    0xFF,0xFF,0xFF,0x3F,0xFF,0xFF,0xFF,0x3F,0xFF,0xFF,0xFF,0x3F,0xFF,0xF9,0xE7,0x3F,
    0xFF,0xF9,0xE7,0x3F,0xFF,0xF9,0xE7,0x3F,0xFF,0xF0,0xE7,0x3F,0x7F,0xE0,0xE7,0x3F,
    0x7F,0xE0,0xC3,0x3F,0x7F,0xE0,0xC3,0x3F,0x7F,0xE0,0xC3,0x3F,0x7F,0xE0,0xE7,0x3F,
    0xFF,0xF0,0xE7,0x3F,0xFF,0xF9,0xE7,0x3F,0xFF,0xF9,0xE7,0x3F,0xFF,0xF9,0xE7,0x3F,
    0xFF,0xF9,0xE7,0x3F,0xFF,0xF9,0xC3,0x3F,0xFF,0xF9,0x81,0x3F,0xFF,0xF0,0x81,0x3F,
    0xFF,0xF0,0x81,0x3F,0xFF,0xF0,0x81,0x3F,0xFF,0xF9,0x81,0x3F,0xFF,0xF9,0xC3,0x3F,
    0xFF,0xF9,0xE7,0x3F,0xFF,0xF9,0xE7,0x3F,0xFF,0xF9,0xE7,0x3F,0xFF,0xFF,0xFF,0x3F,
    0xFF,0xFF,0xFF,0x3F,0xFF,0xFF,0xFF,0x3F
  },
  {
    0xFF,0xFF,0xFF,0x3F,0xFF,0xFF,0xFF,0x3F,0xEF,0xFF,0xFF,0x3F,0xC7,0xFF,0xFF,0x3F,
    0xC7,0xF3,0xFF,0x3F,0x83,0xC0,0xFF,0x3F,0xEF,0xCC,0xFF,0x3F,0x6F,0x9E,0xFF,0x3F,
    0x6F,0x9E,0xFF,0x3F,0x2F,0x3F,0xFF,0x3F,0x2F,0x3F,0xFF,0x3F,0x8F,0x7F,0xFE,0x3F,
    0x8F,0x7F,0xFE,0x39,0x8F,0x7F,0xFE,0x39,0xCF,0xFF,0xFC,0x3C,0xCF,0xFF,0xFC,0x3C,
    0xEF,0xFF,0xFC,0x3C,0xEF,0xFF,0x79,0x3E,0xEF,0xFF,0x79,0x3E,0xEF,0xFF,0x33,0x3F,
    0xEF,0xFF,0x33,0x3F,0xEF,0xFF,0x87,0x3F,0xEF,0xFF,0xCF,0x3F,0xEF,0xFF,0x7F,0x3E,
    0xEF,0xFF,0x7F,0x38,0x0F,0x00,0x00,0x30,0xFF,0xFF,0x7F,0x38,0xFF,0xFF,0x7F,0x3E,
    0xFF,0xFF,0xFF,0x3F,0xFF,0xFF,0xFF,0x3F,
  },
  {
    0xFF,0xFF,0xFF,0x3F,0xFF,0xFF,0xFF,0x3F,0xFF,0xFF,0xFF,0x3F,0xFF,0xFF,0xFF,0x3F,
    0xFF,0x1F,0xFE,0x3F,0xFF,0x1F,0xFE,0x3F,0xFF,0x0C,0xCC,0x3F,0x7F,0x00,0x80,0x3F,
    0x3F,0x00,0x00,0x3F,0x3F,0xE0,0x01,0x3F,0x7F,0xF8,0x87,0x3F,0x7F,0xFC,0x8F,0x3F,
    0x3F,0xFC,0x0F,0x3F,0x0F,0x3E,0x1F,0x3C,0x0F,0x1E,0x1E,0x3C,0x0F,0x1E,0x1E,0x3C,
    0x0F,0x3E,0x1F,0x3C,0x3F,0xFC,0x0F,0x3F,0x7F,0xFC,0x8F,0x3F,0x7F,0xF8,0x87,0x3F,
    0x3F,0xE0,0x01,0x3F,0x3F,0x00,0x00,0x3F,0x7F,0x00,0x80,0x3F,0xFF,0x0C,0xCC,0x3F,
    0xFF,0x1F,0xFE,0x3F,0xFF,0x1F,0xFE,0x3F,0xFF,0xFF,0xFF,0x3F,0xFF,0xFF,0xFF,0x3F,
    0xFF,0xFF,0xFF,0x3F,0xFF,0xFF,0xFF,0x3F
  },
};

/************************************* 页面变量 *************************************/

//OLED变量
#define   DISP_H              64    //屏幕高度
#define   DISP_W              128   //屏幕宽度
uint8_t   *buf_ptr;                 //指向屏幕缓冲的指针
uint16_t  buf_len;                  //缓冲长度

//UI变量
#define   UI_DEPTH            20    //最深层级数
#define   UI_MNUMB            100   //菜单数量
#define   UI_PARAM            16    //参数数量
enum 
{
  DISP_BRI,     //屏幕亮度
  TILE_ANI,     //磁贴动画速度
  LIST_ANI,     //列表动画速度
  WIN_ANI,      //弹窗动画速度
  SPOT_ANI,     //聚光动画速度
  TAG_ANI,      //标签动画速度
  FADE_ANI,     //消失动画速度
  BTN_SPT,      //按键短按时长
  BTN_LPT,      //按键长按时长
  TILE_UFD,     //磁贴图标从头展开开关
  LIST_UFD,     //菜单列表从头展开开关
  TILE_LOOP,    //磁贴图标循环模式开关
  LIST_LOOP,    //菜单列表循环模式开关
  WIN_BOK,      //弹窗背景虚化开关
  KNOB_DIR,     //旋钮方向切换开关
  DARK_MODE,    //黑暗模式开关
};
struct 
{
  bool      init;
  uint8_t   num[UI_MNUMB];
  uint8_t   select[UI_DEPTH];
  uint8_t   layer;
  uint8_t   index = M_MAIN;
  uint8_t   state = S_LAYER_IN;
  bool      sleep = true;
  uint8_t   fade = 1;
  uint8_t   param[UI_PARAM];
} ui;

//磁贴变量
//所有磁贴页面都使用同一套参数
#define   TILE_B_FONT         u8g2_font_helvB18_tr        //磁贴大标题字体
#define   TILE_B_TITLE_H      18                          //磁贴大标题字体高度
#define   TILE_ICON_H         30                          //磁贴图标高度
#define   TILE_ICON_W         30                          //磁贴图标宽度
#define   TILE_ICON_S         36                          //磁贴图标间距
#define   TILE_INDI_H         27                          //磁贴大标题指示器高度
#define   TILE_INDI_W         7                           //磁贴大标题指示器宽度
#define   TILE_INDI_S         36                          //磁贴大标题指示器上边距
struct 
{
  float   title_y_calc = TILE_INDI_S + (TILE_INDI_H - TILE_B_TITLE_H) / 2 + TILE_B_TITLE_H * 2;
  float   title_y_trg_calc = TILE_INDI_S + (TILE_INDI_H - TILE_B_TITLE_H) / 2 + TILE_B_TITLE_H;
  int16_t temp;
  bool    select_flag;
  float   icon_x;
  float   icon_x_trg;
  float   icon_y;
  float   icon_y_trg;
  float   indi_x; 
  float   indi_x_trg;
  float   title_y;
  float   title_y_trg;
} tile;

//列表变量
//默认参数

#define   LIST_FONT           u8g2_font_HelvetiPixel_tr   //列表字体
#define   LIST_TEXT_H         8                           //列表每行文字字体的高度
#define   LIST_LINE_H         16                          //列表单行高度
#define   LIST_TEXT_S         4                           //列表每行文字的上边距,左边距和右边距,下边距由它和字体高度和行高度决定
#define   LIST_BAR_W          5                           //列表进度条宽度,需要是奇数,因为正中间有1像素宽度的线
#define   LIST_BOX_R          0.5                         //列表选择框圆角

/*
//超窄行高度测试
#define   LIST_FONT           u8g2_font_4x6_tr            //列表字体
#define   LIST_TEXT_H         5                           //列表每行文字字体的高度
#define   LIST_LINE_H         7                           //列表单行高度
#define   LIST_TEXT_S         1                           //列表每行文字的上边距,左边距和右边距,下边距由它和字体高度和行高度决定
#define   LIST_BAR_W          7                           //列表进度条宽度,需要是奇数,因为正中间有1像素宽度的线
#define   LIST_BOX_R          0.5                         //列表选择框圆角
*/
struct
{
  uint8_t line_n = DISP_H / LIST_LINE_H;
  int16_t temp;
  bool    loop;
  float   y;
  float   y_trg;
  float   box_x;
  float   box_x_trg;
  float   box_y;
  float   box_y_trg[UI_DEPTH];
  float   bar_y;
  float   bar_y_trg;
} list;

//曲线相关
#define   WAVE_SAMPLE         20                          //采集倍数
#define   WAVE_W              94                          //波形宽度
#define   WAVE_L              24                          //波形左边距
#define   WAVE_U              0                           //波形上边距
#define   WAVE_MAX            27                          //最大值
#define   WAVE_MIN            4                           //最小值
#define   WAVE_BOX_H          32                          //波形边框高度
#define   WAVE_BOX_W          94                          //波形边框宽度
#define   WAVE_BOX_L_S        24                          //波形边框左边距
//列表和文字背景框相关
#define   VOLT_FONT           u8g2_font_helvB18_tr        //电压数字字体
#define   VOLT_TEXT_BG_L_S    24                          //文字背景框左边距
#define   VOLT_TEXT_BG_W      94                          //文字背景框宽度
#define   VOLT_TEXT_BG_H      29                          //文字背景框高度
struct
{
  int     ch0_adc[WAVE_SAMPLE * WAVE_W];
  int     ch0_wave[WAVE_W];
  int     val;
  float   text_bg_r; 
  float   text_bg_r_trg; 
} volt;


//选择框变量

//默认参数
#define   CHECK_BOX_L_S       95                          //选择框在每行的左边距
#define   CHECK_BOX_U_S       2                           //选择框在每行的上边距
#define   CHECK_BOX_F_W       12                          //选择框外框宽度
#define   CHECK_BOX_F_H       12                          //选择框外框高度
#define   CHECK_BOX_D_S       2                           //选择框里面的点距离外框的边距

/*
//超窄行高度测试
#define   CHECK_BOX_L_S       99                          //选择框在每行的左边距
#define   CHECK_BOX_U_S       0                           //选择框在每行的上边距
#define   CHECK_BOX_F_W       5                           //选择框外框宽度
#define   CHECK_BOX_F_H       5                           //选择框外框高度
#define   CHECK_BOX_D_S       1                           //选择框里面的点距离外框的边距
*/
struct
{
  uint8_t *v;
  uint8_t *m;
  uint8_t *s;
  uint8_t *s_p;
} check_box;

//弹窗变量
#define   WIN_FONT            u8g2_font_HelvetiPixel_tr   //弹窗字体
#define   WIN_H               32                          //弹窗高度
#define   WIN_W               102                         //弹窗宽度
#define   WIN_BAR_W           92                          //弹窗进度条宽度
#define   WIN_BAR_H           7                           //弹窗进度条高度
#define   WIN_Y               - WIN_H - 2                 //弹窗竖直方向出场起始位置
#define   WIN_Y_TRG           - WIN_H - 2                 //弹窗竖直方向退场终止位置
struct
{
  //uint8_t
  uint8_t   *value;
  uint8_t   max;
  uint8_t   min;
  uint8_t   step;

  MENU      *bg;
  uint8_t   index;
  char      title[20];
  uint8_t   select;
  uint8_t   l = (DISP_W - WIN_W) / 2;
  uint8_t   u = (DISP_H - WIN_H) / 2;
  float     bar;
  float     bar_trg;
  float     y;
  float     y_trg;
} win;

//聚光灯变量
struct
{
  float   l; 
  float   l_trg; 
  float   r; 
  float   r_trg; 
  float   u; 
  float   u_trg; 
  float   d; 
  float   d_trg; 
} spot;

/********************************** 自定义功能变量 **********************************/

//旋钮功能变量
#define   KNOB_PARAM          4
#define   KNOB_DISABLE        0
#define   KNOB_ROT_VOL        1
#define   KNOB_ROT_BRI        2
enum 
{
  KNOB_ROT,       //睡眠下旋转旋钮的功能,0 禁用,1 音量,2 亮度
  KNOB_COD,       //睡眠下短按旋钮输入的字符码,0 禁用
  KNOB_ROT_P,     //旋转旋钮功能在单选框中选择的位置
  KNOB_COD_P,     //字符码在单选框中选择的位置
};
struct 
{
  uint8_t param[KNOB_PARAM] = { KNOB_DISABLE, KNOB_DISABLE, 2, 2 }; //禁用在列表的第2个选项,第0个是标题,第1个是分界线
} knob;

/************************************* 断电保存 *************************************/

//#include <EEPROM.h>

//EEPROM变量
#define   EEPROM_CHECK        11
struct
{
  bool    init;
  bool    change;
  int     address;
  uint8_t check;
  uint8_t check_param[EEPROM_CHECK] = { 'a', 'b', 'c', 'd', 'e', 'f','g', 'h', 'i', 'j', 'k' }; 
} eeprom;

//EEPROM写数据,回到睡眠时执行一遍
void eeprom_write_all_data()
{
#if 0
  eeprom.address = 0;
  for (uint8_t i = 0; i < EEPROM_CHECK; ++i)    EEPROM.write(eeprom.address + i, eeprom.check_param[i]);  eeprom.address += EEPROM_CHECK;
  for (uint8_t i = 0; i < UI_PARAM; ++i)        EEPROM.write(eeprom.address + i, ui.param[i]);            eeprom.address += UI_PARAM;
  for (uint8_t i = 0; i < KNOB_PARAM; ++i)      EEPROM.write(eeprom.address + i, knob.param[i]);          eeprom.address += KNOB_PARAM;
#else
	nvs_handle handle;
	int32_t err;

	// Open
	err = nvs_open(STORAGE_NAMESPACE, NVS_READWRITE, &handle);
	if (err != ESP_OK) goto err;

	err = nvs_set_blob(handle, "ui_param_ck", eeprom.check_param, EEPROM_CHECK);
	if (err != ESP_OK) goto err;

	err = nvs_set_blob(handle, "ui_param",    ui.param,   		  UI_PARAM);
	if (err != ESP_OK) goto err;

	err = nvs_set_blob(handle, "knob_param",  knob.param, 		  KNOB_PARAM);
	if (err != ESP_OK) goto err;

	
	ESP_LOGI(TAG, "param write all finish!");

	err = nvs_commit(handle);
	if (err != ESP_OK) goto err;

err:
	// Close
	nvs_close(handle);
#endif
}

//EEPROM读数据,开机初始化时执行一遍
void eeprom_read_all_data()
{
#if 0
  eeprom.address = EEPROM_CHECK;   
  for (uint8_t i = 0; i < UI_PARAM; ++i)        ui.param[i]   = EEPROM.read(eeprom.address + i);          eeprom.address += UI_PARAM;
  for (uint8_t i = 0; i < KNOB_PARAM; ++i)      knob.param[i] = EEPROM.read(eeprom.address + i);          eeprom.address += KNOB_PARAM;
#else
	nvs_handle handle;
	size_t length;
	int32_t err;

	// Open
	err = nvs_open(STORAGE_NAMESPACE, NVS_READWRITE, &handle);
	if (err != ESP_OK) goto err;

	err = nvs_get_blob(handle, "ui_param",   NULL, &length);
	err = nvs_get_blob(handle, "ui_param",   ui.param, &length);
	if (err != ESP_OK) goto err;

	err = nvs_get_blob(handle, "knob_param", NULL, &length);
	err = nvs_get_blob(handle, "knob_param", knob.param, &length);
	if (err != ESP_OK) goto err;

	ESP_LOGI(TAG, "param read all finish!");

err:
	// Close
	nvs_close(handle);

#endif
}

//开机检查是否已经修改过,没修改过则跳过读配置步骤,用默认设置
void eeprom_init()
{
#if 0
  eeprom.check = 0;
  eeprom.address = 0; for (uint8_t i = 0; i < EEPROM_CHECK; ++i)  if (EEPROM.read(eeprom.address + i) != eeprom.check_param[i])  eeprom.check ++;
  if (eeprom.check <= 1) eeprom_read_all_data();  //允许一位误码
  else ui_param_init();
#else
	nvs_handle handle;
	size_t length;
	int32_t err;

	uint8_t check_param[EEPROM_CHECK] = {0};

	// Open
	err = nvs_open(STORAGE_NAMESPACE, NVS_READWRITE, &handle);
	if (err != ESP_OK) goto err;

	err = nvs_get_blob(handle, "ui_param_ck", NULL, &length);
	err = nvs_get_blob(handle, "ui_param_ck", check_param, &length);
	
	if (err != ESP_OK) {
		ui_param_init();
		goto err;
	}

	eeprom.check = 0;
	eeprom.address = 0; for (uint8_t i = 0; i < EEPROM_CHECK; ++i)	if (check_param[i] != eeprom.check_param[i])  eeprom.check ++;
	if (eeprom.check <= 1) eeprom_read_all_data();	//允许一位误码
	else ui_param_init();

	ESP_LOGI(TAG, "ui param check pass!");

err:
	// Close
	nvs_close(handle);
#endif
}

/************************************* 旋钮相关 *************************************/

//可按下旋钮引脚
#define   AIO   PB12
#define   BIO   PB13
//#define   SW    PB14
#define   SW    34

//按键ID
#define   BTN_ID_CC           0   //逆时针旋转
#define   BTN_ID_CW           1   //顺时针旋转
#define   BTN_ID_SP           2   //短按
#define   BTN_ID_LP           3   //长按

//按键变量
#define   BTN_PARAM_TIMES     2   //由于uint8_t最大值可能不够,但它存储起来方便,这里放大两倍使用
struct
{
  uint8_t   id;
  bool      flag;
  bool      pressed;
  bool      CW_1;
  bool      CW_2;
  bool      val;
  bool      val_last;  
  bool      alv;  
  bool      blv;
  long      count;
} volatile btn;

void knob_inter() 
{
#if 0
  btn.alv = digitalRead(AIO);
  btn.blv = digitalRead(BIO);
  if (!btn.flag && btn.alv == LOW) 
  {
    btn.CW_1 = btn.blv;
    btn.flag = true;
  }
  if (btn.flag && btn.alv) 
  {
    btn.CW_2 = !btn.blv;
    if (btn.CW_1 && btn.CW_2)
     {
      btn.id = ui.param[KNOB_DIR];
      btn.pressed = true;
    }
    if (btn.CW_1 == false && btn.CW_2 == false) 
    {
      btn.id = !ui.param[KNOB_DIR];
      btn.pressed = true;
    }
    btn.flag = false;
  }
#else
	if (btn.pressed == false) {
	  if (motor_a.pos_spd_ltd.v2 > 60) {
		btn.id = !ui.param[KNOB_DIR];
		btn.pressed = true;
	  } else if (motor_a.pos_spd_ltd.v2 < -60) {
		btn.id = ui.param[KNOB_DIR];
		btn.pressed = true;
	  } else {
		btn.pressed = false;
	  }
	}
#endif
}

void btn_scan() 
{
  btn.val = digitalRead(SW);
  if (btn.val != btn.val_last)
  {
    btn.val_last = btn.val;
    delay(ui.param[BTN_SPT] * BTN_PARAM_TIMES);
    btn.val = digitalRead(SW);
    if (btn.val == LOW)
    {
      btn.pressed = true;
      btn.count = 0;
      while (!digitalRead(SW))
      {
        btn.count++;
        delay(1);
      }
      if (btn.count < ui.param[BTN_LPT] * BTN_PARAM_TIMES)  btn.id = BTN_ID_SP;
      else  btn.id = BTN_ID_LP;
	  ESP_LOGI(TAG, "btn_scan:%d/%d=%d", btn.count, (ui.param[BTN_LPT] * BTN_PARAM_TIMES), btn.id);
    }
  }
}

void btn_init() 
{
#if 0
  pinMode(AIO, INPUT);
  pinMode(BIO, INPUT);
  pinMode(SW, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(AIO), knob_inter, CHANGE);
#else

#endif
}

/************************************ 初始化函数 ***********************************/

/********************************* 初始化数据处理函数 *******************************/

//显示数值的初始化
void check_box_v_init(uint8_t *param)
{
  check_box.v = param;
}

//多选框的初始化
void check_box_m_init(uint8_t *param)
{
  check_box.m = param;
}

//单选框时的初始化
void check_box_s_init(uint8_t *param, uint8_t *param_p)
{
  check_box.s = param;
  check_box.s_p = param_p;
}

//多选框处理函数
void check_box_m_select(uint8_t param)
{
  check_box.m[param] = !check_box.m[param];
  eeprom.change = true;
}

//单选框处理函数
void check_box_s_select(uint8_t val, uint8_t pos)
{
  *check_box.s = val;
  *check_box.s_p = pos;
  eeprom.change = true;
}

//弹窗数值初始化
void window_value_init(char title[], uint8_t select, uint8_t *value, uint8_t max, uint8_t min, uint8_t step, MENU *bg, uint8_t index)
{
  strcpy(win.title, title);
  win.select = select;
  win.value = value;
  win.max = max;
  win.min = min;
  win.step = step;
  win.bg = bg;
  win.index = index;  
  ui.index = M_WINDOW;
  ui.state = S_WINDOW;
}

/*********************************** UI 初始化函数 *********************************/

//在初始化EEPROM时,选择性初始化的默认设置
void ui_param_init()
{
  ui.param[DISP_BRI]  = 255;      //屏幕亮度
  ui.param[TILE_ANI]  = 30;       //磁贴动画速度
  ui.param[LIST_ANI]  = 60;       //列表动画速度
  ui.param[WIN_ANI]   = 25;       //弹窗动画速度
  ui.param[SPOT_ANI]  = 50;       //聚光动画速度
  ui.param[TAG_ANI]   = 60;       //标签动画速度
  ui.param[FADE_ANI]  = 30;       //消失动画速度
  ui.param[BTN_SPT]   = 25;       //按键短按时长
  ui.param[BTN_LPT]   = 150;      //按键长按时长
  ui.param[TILE_UFD]  = 1;        //磁贴图标从头展开开关
  ui.param[LIST_UFD]  = 1;        //菜单列表从头展开开关
  ui.param[TILE_LOOP] = 0;        //磁贴图标循环模式开关
  ui.param[LIST_LOOP] = 0;        //菜单列表循环模式开关
  ui.param[WIN_BOK]   = 0;        //弹窗背景虚化开关
  ui.param[KNOB_DIR]  = 0;        //旋钮方向切换开关   
  ui.param[DARK_MODE] = 1;        //黑暗模式开关   
}

//列表类页面列表行数初始化,必须初始化的参数
void ui_init()
{
  ui.num[M_MAIN]      = sizeof( main_menu     )   / sizeof(M_SELECT);
  ui.num[M_EDITOR]    = sizeof( editor_menu   )   / sizeof(M_SELECT);
  ui.num[M_KNOB]      = sizeof( knob_menu     )   / sizeof(M_SELECT);
  ui.num[M_KRF]       = sizeof( krf_menu      )   / sizeof(M_SELECT);
  ui.num[M_KPF]       = sizeof( kpf_menu      )   / sizeof(M_SELECT);
  ui.num[M_VOLT]      = sizeof( volt_menu     )   / sizeof(M_SELECT);
  ui.num[M_SETTING]   = sizeof( setting_menu  )   / sizeof(M_SELECT);
  ui.num[M_ABOUT]     = sizeof( about_menu    )   / sizeof(M_SELECT);   
}

/********************************* 分页面初始化函数 ********************************/

//进入磁贴类时的初始化
void tile_param_init()
{
  ui.init = false;
  tile.icon_x = 0;
  tile.icon_x_trg = TILE_ICON_S;
  tile.icon_y = -TILE_ICON_H;
  tile.icon_y_trg = 0;
  tile.indi_x = 0;
  tile.indi_x_trg = TILE_INDI_W;
  tile.title_y = tile.title_y_calc;
  tile.title_y_trg = tile.title_y_trg_calc;
}

//进入睡眠时的初始化
void sleep_param_init()
{
//#ifdef ARDUINO
#if 0
  u8g2.setDrawColor(0);
  u8g2.drawBox(0, 0, DISP_W, DISP_H);
  u8g2.setPowerSave(1);
#else
  u8g2_SetDrawColor(&u8g2, 0);
  u8g2_DrawBox(&u8g2, 0, 0, DISP_W, DISP_H);
  //u8g2_SetPowerSave(&u8g2, 1);
#endif
  ui.state = S_NONE;  
  ui.sleep = true;
  if (eeprom.change)
  {
    eeprom_write_all_data();
    eeprom.change = false;
  }
}

//旋钮设置页初始化
void knob_param_init() { check_box_v_init(knob.param); }

//旋钮旋转页初始化
void krf_param_init() { check_box_s_init(&knob.param[KNOB_ROT], &knob.param[KNOB_ROT_P]); }

//旋钮点按页初始化
void kpf_param_init() { check_box_s_init(&knob.param[KNOB_COD], &knob.param[KNOB_COD_P]); }

//电压测量页初始化
void volt_param_init()
{
  volt.text_bg_r = 0;
  volt.text_bg_r_trg = VOLT_TEXT_BG_W; 
}

//设置页初始化
void setting_param_init()
{
  check_box_v_init(ui.param);
  check_box_m_init(ui.param);
}

/********************************** 通用初始化函数 *********************************/

/*
  页面层级管理逻辑是,把所有页面都先当作列表类初始化,不是列表类按需求再初始化对应函数
  这样做会浪费一些资源,但跳转页面时只需要考虑页面层级,逻辑上更清晰,减少出错
*/

//弹窗动画初始化
void window_param_init()
{
  win.bar = 0;
  win.y = WIN_Y;
  win.y_trg = win.u;
  ui.state = S_NONE;
}

//进入更深层级时的初始化
void layer_init_in()
{
  ui.layer ++;
  ui.init = 0;
  list.y = 0;
  list.y_trg = LIST_LINE_H;
  list.box_x = 0;
  list.box_y = 0;
  list.bar_y = 0;
  ui.state = S_FADE;
  switch (ui.index)
  {
    case M_MAIN:    tile_param_init();    break;  //睡眠进入主菜单,动画初始化   
    case M_KNOB:    knob_param_init();    break;  //旋钮设置页,行末尾文字初始化
    case M_KRF:     krf_param_init();     break;  //旋钮旋转页,单选框初始化  
    case M_KPF:     kpf_param_init();     break;  //旋钮点按页,单选框初始化  
    case M_VOLT:    volt_param_init();    break;  //主菜单进入电压测量页,动画初始化
    case M_SETTING: setting_param_init(); break;  //主菜单进入设置页,单选框初始化
  }
}

//进入更浅层级时的初始化
void layer_init_out()
{
  ui.select[ui.layer] = 0;
  list.box_y_trg[ui.layer] = 0;
  ui.layer --;
  ui.init = 0;
  list.y = 0;
  list.y_trg = LIST_LINE_H;
  list.bar_y = 0;
  ui.state = S_FADE;
  switch (ui.index)
  {
    case M_SLEEP: sleep_param_init(); break;    //主菜单进入睡眠页,检查是否需要写EEPROM
    case M_MAIN:  tile_param_init();  break;    //不管什么页面进入主菜单时,动画初始化
  }
}

/************************************* 动画函数 *************************************/

//动画函数
void animation(float *a, float *a_trg, uint8_t n)
{
  if (fabs(*a - *a_trg) < 0.15) *a = *a_trg;
  if (*a != *a_trg) *a += (*a_trg - *a) / (ui.param[n] / 10.0);
}

//消失函数
void fade()
{
  delay(ui.param[FADE_ANI]);
  if (ui.param[DARK_MODE])
  {
    switch (ui.fade)
    {
      case 1: for (uint16_t i = 0; i < buf_len; ++i)  if (i % 2 != 0) buf_ptr[i] = buf_ptr[i] & 0xAA; break;
      case 2: for (uint16_t i = 0; i < buf_len; ++i)  if (i % 2 != 0) buf_ptr[i] = buf_ptr[i] & 0x00; break;
      case 3: for (uint16_t i = 0; i < buf_len; ++i)  if (i % 2 == 0) buf_ptr[i] = buf_ptr[i] & 0x55; break;
      case 4: for (uint16_t i = 0; i < buf_len; ++i)  if (i % 2 == 0) buf_ptr[i] = buf_ptr[i] & 0x00; break;
      default: ui.state = S_NONE; ui.fade = 0; break;
    }
  }
  else
  {
    switch (ui.fade)
    {
      case 1: for (uint16_t i = 0; i < buf_len; ++i)  if (i % 2 != 0) buf_ptr[i] = buf_ptr[i] | 0xAA; break;
      case 2: for (uint16_t i = 0; i < buf_len; ++i)  if (i % 2 != 0) buf_ptr[i] = buf_ptr[i] | 0x00; break;
      case 3: for (uint16_t i = 0; i < buf_len; ++i)  if (i % 2 == 0) buf_ptr[i] = buf_ptr[i] | 0x55; break;
      case 4: for (uint16_t i = 0; i < buf_len; ++i)  if (i % 2 == 0) buf_ptr[i] = buf_ptr[i] | 0x00; break;
      default: ui.state = S_NONE; ui.fade = 0; break;
    }    
  }
  ui.fade++;
}

/************************************* 显示函数 *************************************/

//磁贴类页面通用显示函数
void tile_show(struct MENU arr_1[], const uint8_t icon_pic[][120])
{
  //计算动画过渡值
  animation(&tile.icon_x, &tile.icon_x_trg, TILE_ANI);
  animation(&tile.icon_y, &tile.icon_y_trg, TILE_ANI);
  animation(&tile.indi_x, &tile.indi_x_trg, TILE_ANI);
  animation(&tile.title_y, &tile.title_y_trg, TILE_ANI);

  //设置大标题的颜色,0透显,1实显,2反色,这里用实显
  //u8g2.setDrawColor(1);
  u8g2_SetDrawColor(&u8g2, 1);

  //绘制大标题
  //u8g2.setFont(TILE_B_FONT); 
  //u8g2.drawStr(((DISP_W - TILE_INDI_W) - u8g2.getStrWidth(arr_1[ui.select[ui.layer]].m_select)) / 2 + TILE_INDI_W, tile.title_y, arr_1[ui.select[ui.layer]].m_select);
  u8g2_SetFont(&u8g2, TILE_B_FONT);
  u8g2_DrawStr(&u8g2, ((DISP_W - TILE_INDI_W) - u8g2_GetStrWidth(&u8g2, arr_1[ui.select[ui.layer]].m_select)) / 2 + TILE_INDI_W, tile.title_y, arr_1[ui.select[ui.layer]].m_select);

  //绘制大标题指示器
  //u8g2.drawBox(0, TILE_ICON_S, tile.indi_x, TILE_INDI_H);
  u8g2_DrawBox(&u8g2, 0, TILE_ICON_S, tile.indi_x, TILE_INDI_H); 

  //绘制图标
  if (!ui.init)
  {
    for (uint8_t i = 0; i < ui.num[ui.index]; ++i)  
    {
      if (ui.param[TILE_UFD]) tile.temp = (DISP_W - TILE_ICON_W) / 2 + i * tile.icon_x - TILE_ICON_S * ui.select[ui.layer];
      else tile.temp = (DISP_W - TILE_ICON_W) / 2 + (i - ui.select[ui.layer]) * tile.icon_x;
      //u8g2.drawXBMP(tile.temp, (int16_t)tile.icon_y, TILE_ICON_W, TILE_ICON_H, icon_pic[i]); 
      u8g2_DrawXBMP(&u8g2, tile.temp, (int16_t)tile.icon_y, TILE_ICON_W, TILE_ICON_H, icon_pic[i]); 
    }
    if (tile.icon_x == tile.icon_x_trg) 
    {
      ui.init = true;
      tile.icon_x = tile.icon_x_trg = - ui.select[ui.layer] * TILE_ICON_S;
    }
  }
  else for (uint8_t i = 0; i < ui.num[ui.index]; ++i) 
  	//u8g2.drawXBMP((DISP_W - TILE_ICON_W) / 2 + (int16_t)tile.icon_x + i * TILE_ICON_S, 0, TILE_ICON_W, TILE_ICON_H, icon_pic[i]);
  	u8g2_DrawXBMP(&u8g2, (DISP_W - TILE_ICON_W) / 2 + (int16_t)tile.icon_x + i * TILE_ICON_S, 0, TILE_ICON_W, TILE_ICON_H, icon_pic[i]);


  //反转屏幕内元素颜色,白天模式遮罩
  //u8g2.setDrawColor(2);
  u8g2_SetDrawColor(&u8g2, 2);
  if (!ui.param[DARK_MODE]) 
  	//u8g2.drawBox(0, 0, DISP_W, DISP_H);
  	u8g2_DrawBox(&u8g2, 0, 0, DISP_W, DISP_H);
}

static u8g2_uint_t u8g2_cursor_x = 0;
static u8g2_uint_t u8g2_cursor_y = 0;

static void u8g2_setCursor(u8g2_uint_t x, u8g2_uint_t y) {
	u8g2_cursor_x = x;
	u8g2_cursor_y = y;
}

static u8g2_uint_t u8g2_print(const char *str) { 
	u8g2_uint_t delta = u8g2_DrawStr(&u8g2, u8g2_cursor_x, u8g2_cursor_y, str);
	u8g2_cursor_x += delta;
	if (u8g2_cursor_x > DISP_W) u8g2_cursor_x=0;
	return delta;
}

#define DEC 10
#define HEX 16
#define OCT 8
#define BIN 2
static u8g2_uint_t printNumber(unsigned long n, uint8_t base)
{
    char buf[8 * sizeof(n) + 1]; // Assumes 8-bit chars plus zero byte.
    char *str = &buf[sizeof(buf) - 1];

    *str = '\0';

    // prevent crash if called with base == 1
    if(base < 2) {
        base = 10;
    }

    do {
        char c = n % base;
        n /= base;

        *--str = c < 10 ? c + '0' : c + 'A' - 10;
    } while (n);

    return u8g2_print(str);
}

static u8g2_uint_t printNumber(long n, uint8_t base)
{
    int t = 0;
    if (base == 10 && n < 0) {
        t = u8g2_print("-");
        n = -n;
    }
	
    return printNumber(static_cast<unsigned long>(n), base);
}

static u8g2_uint_t u8g2_print(char value) { 
	return printNumber((long)value, DEC);
}

static u8g2_uint_t u8g2_print(unsigned char value) { 
	return printNumber((unsigned long)value, DEC);
}

static u8g2_uint_t u8g2_print(int value) { 
	return printNumber((long)value, DEC);
}

static u8g2_uint_t u8g2_print(unsigned int value) { 
	return printNumber((unsigned long)value, DEC);
}

static u8g2_uint_t u8g2_print(long value) { 
	return printNumber(value, DEC);
}

static u8g2_uint_t u8g2_print(unsigned long value) { 
	return printNumber(value, DEC);
}

static u8g2_uint_t printFloat(double number, uint8_t digits) {

    u8g2_uint_t n = 0;

    if(isnan(number)) {
        return u8g2_print("nan");
    }
    if(isinf(number)) {
        return u8g2_print("inf");
    }
    if(number > 4294967040.0) {
        return u8g2_print("ovf");    // constant determined empirically
    }
    if(number < -4294967040.0) {
        return u8g2_print("ovf");    // constant determined empirically
    }

    // Handle negative numbers
    if(number < 0.0) {
        n += u8g2_print("-");
        number = -number;
    }

    // Round correctly so that print(1.999, 2) prints as "2.00"
    double rounding = 0.5;
    for(uint8_t i = 0; i < digits; ++i) {
        rounding /= 10.0;
    }

    number += rounding;

    // Extract the integer part of the number and print it
    unsigned long int_part = (unsigned long) number;
    double remainder = number - (double) int_part;
    n += u8g2_print(int_part);

    // Print the decimal point, but only if there are digits beyond
    if(digits > 0) {
        n += u8g2_print(".");
    }

    // Extract digits from the remainder one at a time
    while(digits-- > 0) {
        remainder *= 10.0;
        int toPrint = int(remainder);
        n += u8g2_print(toPrint);
        remainder -= toPrint;
    }

    return n;
}

static u8g2_uint_t u8g2_print(double value) { 
	return printFloat(value, 3);
}


/*************** 根据列表每行开头符号,判断每行尾部是否绘制以及绘制什么内容 *************/
//列表显示数值
void list_draw_value(int n) { 
	//u8g2.print(check_box.v[n - 1]); 
	u8g2_print(check_box.v[n - 1]); 
}

//绘制外框
void list_draw_check_box_frame() { 
	//u8g2.drawRFrame(CHECK_BOX_L_S, list.temp + CHECK_BOX_U_S, CHECK_BOX_F_W, CHECK_BOX_F_H, 1);
	u8g2_DrawRFrame(&u8g2, CHECK_BOX_L_S, list.temp + CHECK_BOX_U_S, CHECK_BOX_F_W, CHECK_BOX_F_H, 1);
}

//绘制框里面的点
void list_draw_check_box_dot() { 
	//u8g2.drawBox(CHECK_BOX_L_S + CHECK_BOX_D_S + 1, list.temp + CHECK_BOX_U_S + CHECK_BOX_D_S + 1, CHECK_BOX_F_W - (CHECK_BOX_D_S + 1) * 2, CHECK_BOX_F_H - (CHECK_BOX_D_S + 1) * 2); 
	u8g2_DrawBox(&u8g2, CHECK_BOX_L_S + CHECK_BOX_D_S + 1, list.temp + CHECK_BOX_U_S + CHECK_BOX_D_S + 1, CHECK_BOX_F_W - (CHECK_BOX_D_S + 1) * 2, CHECK_BOX_F_H - (CHECK_BOX_D_S + 1) * 2); 
}

//列表显示旋钮功能
void list_draw_krf(int n) 
{ 
  switch (check_box.v[n - 1])
  {
    case 0: u8g2_print("OFF"); break;
    case 1: u8g2_print("VOL"); break;
    case 2: u8g2_print("BRI"); break;
  }
}

//列表显示按键键值
void list_draw_kpf(int n) 
{ 
  if (check_box.v[n - 1] == 0) u8g2_print("OFF");
  else if (check_box.v[n - 1] <= 90) u8g2_print((char)check_box.v[n - 1]);
  else u8g2_print("?");
}

//判断列表尾部内容
void list_draw_text_and_check_box(struct MENU arr[], int i)
{
  //u8g2.drawStr(LIST_TEXT_S, list.temp + LIST_TEXT_H + LIST_TEXT_S, arr[i].m_select);
  //u8g2.setCursor(CHECK_BOX_L_S, list.temp + LIST_TEXT_H + LIST_TEXT_S);
  u8g2_DrawStr(&u8g2, LIST_TEXT_S, list.temp + LIST_TEXT_H + LIST_TEXT_S, arr[i].m_select);
  u8g2_setCursor(CHECK_BOX_L_S, list.temp + LIST_TEXT_H + LIST_TEXT_S);

  switch (arr[i].m_select[0])
  {
    case '~': list_draw_value(i); break;
    case '+': list_draw_check_box_frame(); if (check_box.m[i - 1] == 1)  list_draw_check_box_dot(); break;
    case '=': list_draw_check_box_frame(); if (*check_box.s_p == i)      list_draw_check_box_dot(); break;
    case '#': list_draw_krf(i);   break;
    case '$': list_draw_kpf(i);   break;
  }
}

/******************************** 列表显示函数 **************************************/

//列表类页面通用显示函数
void list_show(struct MENU arr[], uint8_t ui_index)
{
  //更新动画目标值
  //u8g2.setFont(LIST_FONT);
  u8g2_SetFont(&u8g2, LIST_FONT);
  
  //list.box_x_trg = u8g2.getStrWidth(arr[ui.select[ui.layer]].m_select) + LIST_TEXT_S * 2;
  list.box_x_trg = u8g2_GetStrWidth(&u8g2, arr[ui.select[ui.layer]].m_select) + LIST_TEXT_S * 2; 
  list.bar_y_trg = ceil((ui.select[ui.layer]) * ((float)DISP_H / (ui.num[ui_index] - 1)));
  
  //计算动画过渡值
  animation(&list.y, &list.y_trg, LIST_ANI);
  animation(&list.box_x, &list.box_x_trg, LIST_ANI);
  animation(&list.box_y, &list.box_y_trg[ui.layer], LIST_ANI);
  animation(&list.bar_y, &list.bar_y_trg, LIST_ANI);

  //检查循环动画是否结束
  if (list.loop && list.box_y == list.box_y_trg[ui.layer]) list.loop = false;

  //设置文字和进度条颜色,0透显,1实显,2反色,这里都用实显
  //u8g2.setDrawColor(1);
  u8g2_SetDrawColor(&u8g2, 1);
  
  //绘制进度条
  #if 0
  u8g2.drawHLine(DISP_W - LIST_BAR_W, 0, LIST_BAR_W);
  u8g2.drawHLine(DISP_W - LIST_BAR_W, DISP_H - 1, LIST_BAR_W);
  u8g2.drawVLine(DISP_W - ceil((float)LIST_BAR_W / 2), 0, DISP_H);
  u8g2.drawBox(DISP_W - LIST_BAR_W, 0, LIST_BAR_W, list.bar_y);
  #else
  u8g2_DrawHLine(&u8g2, DISP_W - LIST_BAR_W, 0, LIST_BAR_W);
  u8g2_DrawHLine(&u8g2, DISP_W - LIST_BAR_W, DISP_H - 1, LIST_BAR_W);
  u8g2_DrawVLine(&u8g2, DISP_W - ceil((float)LIST_BAR_W / 2), 0, DISP_H);
  u8g2_DrawBox(&u8g2, DISP_W - LIST_BAR_W, 0, LIST_BAR_W, list.bar_y);
  #endif

  //绘制列表文字  
  if (!ui.init)
  {
    for (int i = 0; i < ui.num[ui_index]; ++ i)
    {
      if (ui.param[LIST_UFD]) list.temp = i * list.y - LIST_LINE_H * ui.select[ui.layer] + list.box_y_trg[ui.layer];
      else list.temp = (i - ui.select[ui.layer]) * list.y + list.box_y_trg[ui.layer];
      list_draw_text_and_check_box(arr, i);
    }
    if (list.y == list.y_trg) 
    {
      ui.init = true;
      list.y = list.y_trg = - LIST_LINE_H * ui.select[ui.layer] + list.box_y_trg[ui.layer];
    }
  }
  else for (int i = 0; i < ui.num[ui_index]; ++ i)
  {
    list.temp = LIST_LINE_H * i + list.y;
    list_draw_text_and_check_box(arr, i);
  }

  //绘制文字选择框,0透显,1实显,2反色,这里用反色
  //u8g2.setDrawColor(2);
  //u8g2.drawRBox(0, list.box_y, list.box_x, LIST_LINE_H, LIST_BOX_R);
  u8g2_SetDrawColor(&u8g2, 2);
  u8g2_DrawRBox(&u8g2, 0, list.box_y, list.box_x, LIST_LINE_H, LIST_BOX_R);

  //反转屏幕内元素颜色,白天模式遮罩,在这里屏蔽有列表参与的页面,使遮罩作用在那个页面上
  if (!ui.param[DARK_MODE])
  {
    //u8g2.drawBox(0, 0, DISP_W, DISP_H);
    u8g2_DrawBox(&u8g2, 0, 0, DISP_W, DISP_H);
    switch(ui.index)
    {
      case M_WINDOW: 
      case M_VOLT:
      //u8g2.drawBox(0, 0, DISP_W, DISP_H);  
      u8g2_DrawBox(&u8g2, 0, 0, DISP_W, DISP_H);  
    }
  }
}

//电压页面显示函数
void volt_show()
{
  //使用列表类显示选项
  list_show(volt_menu, M_VOLT); 

  //计算动画过渡值  
  animation(&volt.text_bg_r, &volt.text_bg_r_trg, TAG_ANI);

  //设置曲线颜色,0透显,1实显,2反色,这里用实显
  //u8g2.setDrawColor(1); 
  u8g2_SetDrawColor(&u8g2, 1);

  //绘制电压曲线和外框
  volt.val = 0;
  //u8g2.drawFrame(WAVE_BOX_L_S, 0, WAVE_BOX_W, WAVE_BOX_H);
  //u8g2.drawFrame(WAVE_BOX_L_S + 1, 1, WAVE_BOX_W - 2, WAVE_BOX_H - 2);
  u8g2_DrawFrame(&u8g2, WAVE_BOX_L_S, 0, WAVE_BOX_W, WAVE_BOX_H);
  u8g2_DrawFrame(&u8g2, WAVE_BOX_L_S + 1, 1, WAVE_BOX_W - 2, WAVE_BOX_H - 2);

  if (list.box_y == list.box_y_trg[ui.layer] && list.y == list.y_trg)
  {
    for (int i = 0; i < WAVE_SAMPLE * WAVE_W; i++) volt.ch0_adc[i] = volt.val = (int)(motor_b.pos_spd_ltd.v2);//(int)(adc_data.voltage_mv);//analogRead(analog_pin[ui.select[ui.layer]]);
    for (int i = 1; i < WAVE_W - 1; i++)
    { 
      //volt.ch0_wave[i] = lv_map(volt.ch0_adc[int(5 * i)], 0, 4095, WAVE_MAX, WAVE_MIN);   
      //u8g2.drawLine(WAVE_L + i - 1, WAVE_U + volt.ch0_wave[i - 1], WAVE_L + i, WAVE_U + volt.ch0_wave[i]);
	  volt.ch0_wave[i] = lv_map(volt.ch0_adc[int(5 * i)], -5000, 5000, WAVE_MIN, WAVE_MAX); 
	  u8g2_DrawLine(&u8g2, WAVE_L + i - 1, WAVE_U + volt.ch0_wave[i - 1], WAVE_L + i, WAVE_U + volt.ch0_wave[i]);
    }
  }

#if 0
  //绘制电压值
  u8g2.setFontDirection(0);
  u8g2.setFont(VOLT_FONT); 
  u8g2.setCursor(39, DISP_H - 6);
  u8g2.print(volt.val / 4096.0 * 3.3);
  u8g2.print("V");

  //绘制列表选择框和电压文字背景
  u8g2.setDrawColor(2);
  u8g2.drawBox(VOLT_TEXT_BG_L_S, DISP_H - VOLT_TEXT_BG_H, volt.text_bg_r, VOLT_TEXT_BG_H);

  //反转屏幕内元素颜色,白天模式遮罩
  if (!ui.param[DARK_MODE]) u8g2.drawBox(0, 0, DISP_W, DISP_H);
#else
	//绘制电压值
	u8g2_SetFontDirection(&u8g2, 0);
	u8g2_SetFont(&u8g2, VOLT_FONT); 
	u8g2_setCursor(32, DISP_H - 6);
	//u8g2_print(volt.val / 4096.0 * 3.3);
	//u8g2_print(volt.val/1000.0);
	//u8g2_print(motor_b.pos_spd_ltd.v2/1000.0);
	u8g2_print(volt.val);
	//u8g2_SetFont(&u8g2, LIST_FONT); 
	u8g2_print("V");

	//绘制列表选择框和电压文字背景
	u8g2_SetDrawColor(&u8g2, 2);
	u8g2_DrawBox(&u8g2, VOLT_TEXT_BG_L_S, DISP_H - VOLT_TEXT_BG_H, volt.text_bg_r, VOLT_TEXT_BG_H);

	//反转屏幕内元素颜色,白天模式遮罩
	if (!ui.param[DARK_MODE]) u8g2_DrawBox(&u8g2, 0, 0, DISP_W, DISP_H);
#endif
}

//弹窗通用显示函数
void window_show()
{
  //绘制背景列表,根据开关判断背景是否要虚化
  list_show(win.bg, win.index);
  if (ui.param[WIN_BOK]) for (uint16_t i = 0; i < buf_len; ++i)  buf_ptr[i] = buf_ptr[i] & (i % 2 == 0 ? 0x55 : 0xAA);

  //更新动画目标值
  //u8g2.setFont(WIN_FONT);
  u8g2_SetFont(&u8g2, WIN_FONT);
  win.bar_trg = (float)(*win.value - win.min) / (float)(win.max - win.min) * (WIN_BAR_W - 4);

  //计算动画过渡值
  animation(&win.bar, &win.bar_trg, WIN_ANI);
  animation(&win.y, &win.y_trg, WIN_ANI);

#if 0
  //绘制窗口
  u8g2.setDrawColor(0); u8g2.drawRBox(win.l, (int16_t)win.y, WIN_W, WIN_H, 2);    //绘制外框背景
  u8g2.setDrawColor(1); u8g2.drawRFrame(win.l, (int16_t)win.y, WIN_W, WIN_H, 2);  //绘制外框描边
  u8g2.drawRFrame(win.l + 5, (int16_t)win.y + 20, WIN_BAR_W, WIN_BAR_H, 1);       //绘制进度条外框
  u8g2.drawBox(win.l + 7, (int16_t)win.y + 22, win.bar, WIN_BAR_H - 4);           //绘制进度条
  u8g2.setCursor(win.l + 5, (int16_t)win.y + 14); u8g2.print(win.title);          //绘制标题
  u8g2.setCursor(win.l + 78, (int16_t)win.y + 14); u8g2.print(*win.value);        //绘制当前值
  
  //需要在窗口修改参数时立即见效的函数
  if (!strcmp(win.title, "Disp Bri")) u8g2.setContrast(ui.param[DISP_BRI]);

  //反转屏幕内元素颜色,白天模式遮罩
  u8g2.setDrawColor(2);
  if (!ui.param[DARK_MODE]) u8g2.drawBox(0, 0, DISP_W, DISP_H);
#else
	//绘制窗口
	u8g2_SetDrawColor(&u8g2, 0); u8g2_DrawRBox(&u8g2, win.l, (int16_t)win.y, WIN_W, WIN_H, 2);	//绘制外框背景
	u8g2_SetDrawColor(&u8g2, 1); u8g2_DrawRFrame(&u8g2, win.l, (int16_t)win.y, WIN_W, WIN_H, 2);	//绘制外框描边
	u8g2_DrawRFrame(&u8g2, win.l + 5, (int16_t)win.y + 20, WIN_BAR_W, WIN_BAR_H, 1);		//绘制进度条外框
	u8g2_DrawBox(&u8g2, win.l + 7, (int16_t)win.y + 22, win.bar, WIN_BAR_H - 4);			//绘制进度条
	u8g2_setCursor(win.l + 5, (int16_t)win.y + 14); u8g2_print(win.title);			//绘制标题
	u8g2_setCursor(win.l + 78, (int16_t)win.y + 14); u8g2_print(*win.value);		//绘制当前值

	//需要在窗口修改参数时立即见效的函数
	//if (!strcmp(win.title, "Disp Bri")) u8g2_SetContrast(&u8g2, ui.param[DISP_BRI]);

	//反转屏幕内元素颜色,白天模式遮罩
	u8g2_SetDrawColor(&u8g2, 2);
	if (!ui.param[DARK_MODE]) u8g2_DrawBox(&u8g2, 0, 0, DISP_W, DISP_H);

#endif
}

/************************************* 处理函数 *************************************/

/*********************************** 通用处理函数 ***********************************/

//磁贴类页面旋转时判断通用函数
void tile_rotate_switch()
{
  switch (btn.id)
  { 
    case BTN_ID_CC:
      if (ui.init)
      {
        if (ui.select[ui.layer] > 0)
        {
          ui.select[ui.layer] -= 1;
          tile.icon_x_trg += TILE_ICON_S;
          tile.select_flag = false;
        }
        else 
        {
          if (ui.param[TILE_LOOP])
          {
            ui.select[ui.layer] = ui.num[ui.index] - 1;
            tile.icon_x_trg = - TILE_ICON_S * (ui.num[ui.index] - 1);
            break;
          }
          else tile.select_flag = true;
        }
      }
      break;

    case BTN_ID_CW:
      if (ui.init)
      {
        if (ui.select[ui.layer] < (ui.num[ui.index] - 1)) 
        {
          ui.select[ui.layer] += 1;
          tile.icon_x_trg -= TILE_ICON_S;
          tile.select_flag = false;
        }
        else 
        {
          if (ui.param[TILE_LOOP])
          {
            ui.select[ui.layer] = 0;
            tile.icon_x_trg = 0;
            break;
          }
          else tile.select_flag = true;
        }
      }
      break;
  }
}

//列表类页面旋转时判断通用函数
void list_rotate_switch()
{
  if (!list.loop)
  {
    switch (btn.id)
    {
      case BTN_ID_CC:
        if (ui.select[ui.layer] == 0)
        {
          if (ui.param[LIST_LOOP] && ui.init)
          {
            list.loop = true;
            ui.select[ui.layer] = ui.num[ui.index] - 1;
            if (ui.num[ui.index] > list.line_n) 
            {
              list.box_y_trg[ui.layer] = DISP_H - LIST_LINE_H;
              list.y_trg = DISP_H - ui.num[ui.index] * LIST_LINE_H;
            }
            else list.box_y_trg[ui.layer] = (ui.num[ui.index] - 1) * LIST_LINE_H;
            break;
          }
          else break;
        }
        if (ui.init)
        {
          ui.select[ui.layer] -= 1;
          if (ui.select[ui.layer] < - (list.y_trg / LIST_LINE_H)) 
          {
            if (!(DISP_H % LIST_LINE_H)) list.y_trg += LIST_LINE_H;
            else
            {
              if (list.box_y_trg[ui.layer] == DISP_H - LIST_LINE_H * list.line_n)
              {
                list.y_trg += (list.line_n + 1) * LIST_LINE_H - DISP_H;
                list.box_y_trg[ui.layer] = 0;
              }
              else if (list.box_y_trg[ui.layer] == LIST_LINE_H)
              {
                list.box_y_trg[ui.layer] = 0;
              }
              else list.y_trg += LIST_LINE_H;
            }
          }
          else list.box_y_trg[ui.layer] -= LIST_LINE_H;
          break;
        }

      case BTN_ID_CW:
        if (ui.select[ui.layer] == (ui.num[ui.index] - 1))
        {
          if (ui.param[LIST_LOOP] && ui.init)
          {
            list.loop = true;
            ui.select[ui.layer] = 0;
            list.y_trg = 0;
            list.box_y_trg[ui.layer] = 0;
            break;
          }
          else break;
        }
        if (ui.init)
        {
          ui.select[ui.layer] += 1;
          if ((ui.select[ui.layer] + 1) > (list.line_n - list.y_trg / LIST_LINE_H))
          {
            if (!(DISP_H % LIST_LINE_H)) list.y_trg -= LIST_LINE_H;
            else
            {
              if (list.box_y_trg[ui.layer] == LIST_LINE_H * (list.line_n - 1))
              {
                list.y_trg -= (list.line_n + 1) * LIST_LINE_H - DISP_H;
                list.box_y_trg[ui.layer] = DISP_H - LIST_LINE_H;
              }
              else if (list.box_y_trg[ui.layer] == DISP_H - LIST_LINE_H * 2)
              {
                list.box_y_trg[ui.layer] = DISP_H - LIST_LINE_H;
              }
              else list.y_trg -= LIST_LINE_H;
            }
          }
          else list.box_y_trg[ui.layer] += LIST_LINE_H;
          break;
        }
        break;
    }
  }
}

//弹窗通用处理函数
void window_proc()
{
  window_show();
  if (win.y == WIN_Y_TRG) ui.index = win.index;
  if (btn.pressed && win.y == win.y_trg && win.y != WIN_Y_TRG)
  {
    btn.pressed = false;
    switch (btn.id)
    {
      case BTN_ID_CC: if (*win.value < win.max)  *win.value += win.step;  eeprom.change = true;  break;
      case BTN_ID_CW: if (*win.value > win.min)  *win.value -= win.step;  eeprom.change = true;  break;  
      case BTN_ID_SP: case BTN_ID_LP: win.y_trg = WIN_Y_TRG; break;
    }
  }
}

/********************************** 分页面处理函数 **********************************/

//睡眠页面处理函数
void sleep_proc()
{
  while (ui.sleep)
  {
    //睡眠时循环执行的函数

    //睡眠时需要扫描旋钮才能退出睡眠
    btn_scan();

	delay(1);

    //当旋钮有动作时
    if (btn.pressed) { 
		btn.pressed = false; 
		switch (btn.id) {    
#if 0
        //睡眠时顺时针旋转执行的函数
        case BTN_ID_CW:
          switch (knob.param[KNOB_ROT])
          {
            case KNOB_ROT_VOL: Consumer.press(HIDConsumer::VOLUME_UP);       Consumer.release(); break;
            case KNOB_ROT_BRI: Consumer.press(HIDConsumer::BRIGHTNESS_UP);   Consumer.release(); break;
          }
          break;

        //睡眠时逆时针旋转执行的函数
        case BTN_ID_CC:
          switch (knob.param[KNOB_ROT])
          {
            case KNOB_ROT_VOL: Consumer.press(HIDConsumer::VOLUME_DOWN);     Consumer.release(); break;
            case KNOB_ROT_BRI: Consumer.press(HIDConsumer::BRIGHTNESS_DOWN); Consumer.release(); break;
          }
          break;

        //睡眠时短按执行的函数
        case BTN_ID_SP: Keyboard.press(knob.param[KNOB_COD]);  Keyboard.release(knob.param[KNOB_COD]); break;   
#endif        
        //睡眠时长按执行的函数
        //case BTN_ID_LP: ui.index = M_MAIN;  ui.state = S_LAYER_IN; u8g2.setPowerSave(0); ui.sleep = false; break;
        case BTN_ID_LP: ui.index = M_MAIN;  ui.state = S_LAYER_IN; /*u8g2_SetPowerSave(&u8g2, 0);*/ ui.sleep = false; break;
      }
    }
  }
}

//主菜单处理函数,磁贴类模板
void main_proc()
{
  tile_show(main_menu, main_icon_pic);
  if (btn.pressed) { btn.pressed = false; switch (btn.id) { case BTN_ID_CW: case BTN_ID_CC: tile_rotate_switch(); break; case BTN_ID_SP: switch (ui.select[ui.layer]) {

        case 0: ui.index = M_SLEEP;   ui.state = S_LAYER_OUT; break;
        case 1: ui.index = M_EDITOR;  ui.state = S_LAYER_IN;  break;
        case 2: ui.index = M_VOLT;    ui.state = S_LAYER_IN;  break;
        case 3: ui.index = M_SETTING; ui.state = S_LAYER_IN;  break;
      }
    }
    if (!tile.select_flag && ui.init) { tile.indi_x = 0; tile.title_y = tile.title_y_calc; }
  }
}

//编辑器菜单处理函数
void editor_proc()
{
  list_show(editor_menu, M_EDITOR); 
  if (btn.pressed) { btn.pressed = false; switch (btn.id) { case BTN_ID_CW: case BTN_ID_CC: list_rotate_switch(); break; case BTN_ID_LP: ui.select[ui.layer] = 0; case BTN_ID_SP: switch (ui.select[ui.layer]) {
        
        case 0:   ui.index = M_MAIN;  ui.state = S_LAYER_OUT; break;
        case 11:  ui.index = M_KNOB;  ui.state = S_LAYER_IN;  break;
      }
    }
  }
}

//旋钮编辑菜单处理函数
void knob_proc()
{
  list_show(knob_menu, M_KNOB);
  if (btn.pressed) { btn.pressed = false; switch (btn.id) { case BTN_ID_CW: case BTN_ID_CC: list_rotate_switch(); break; case BTN_ID_LP: ui.select[ui.layer] = 0; case BTN_ID_SP: switch (ui.select[ui.layer]) {
        
        case 0: ui.index = M_EDITOR;  ui.state = S_LAYER_OUT; break;
        case 1: ui.index = M_KRF;     ui.state = S_LAYER_IN;  check_box_s_init(&knob.param[KNOB_ROT], &knob.param[KNOB_ROT_P]); break;
        case 2: ui.index = M_KPF;     ui.state = S_LAYER_IN;  check_box_s_init(&knob.param[KNOB_COD], &knob.param[KNOB_COD_P]); break;
      }
    }
  }
}

//旋钮旋转功能菜单处理函数
void krf_proc()
{
  list_show(krf_menu, M_KRF);
  if (btn.pressed) { btn.pressed = false; switch (btn.id) { case BTN_ID_CW: case BTN_ID_CC: list_rotate_switch(); break; case BTN_ID_LP: ui.select[ui.layer] = 0; case BTN_ID_SP: switch (ui.select[ui.layer]) {
        
        case 0: ui.index = M_KNOB;  ui.state = S_LAYER_OUT; break;
        case 1: break;
        case 2: check_box_s_select(KNOB_DISABLE, ui.select[ui.layer]); break;
        case 3: break;
        case 4: check_box_s_select(KNOB_ROT_VOL, ui.select[ui.layer]); break;
        case 5: check_box_s_select(KNOB_ROT_BRI, ui.select[ui.layer]); break;
        case 6: break;
      }
    }
  }
}

#define KEY_LEFT_CTRL   0x80
#define KEY_LEFT_SHIFT  0x81
#define KEY_LEFT_ALT    0x82
#define KEY_LEFT_GUI    0x83
#define KEY_RIGHT_CTRL  0x84
#define KEY_RIGHT_SHIFT 0x85
#define KEY_RIGHT_ALT   0x86
#define KEY_RIGHT_GUI   0x87

#define KEY_UP_ARROW    0xDA
#define KEY_DOWN_ARROW  0xD9
#define KEY_LEFT_ARROW  0xD8
#define KEY_RIGHT_ARROW 0xD7
#define KEY_BACKSPACE   0xB2
#define KEY_TAB         0xB3
#define KEY_RETURN      0xB0
#define KEY_ESC         0xB1
#define KEY_INSERT      0xD1
#define KEY_DELETE      0xD4
#define KEY_PAGE_UP     0xD3
#define KEY_PAGE_DOWN   0xD6
#define KEY_HOME        0xD2
#define KEY_END         0xD5
#define KEY_CAPS_LOCK   0xC1
#define KEY_F1          0xC2
#define KEY_F2          0xC3
#define KEY_F3          0xC4
#define KEY_F4          0xC5
#define KEY_F5          0xC6
#define KEY_F6          0xC7
#define KEY_F7          0xC8
#define KEY_F8          0xC9
#define KEY_F9          0xCA
#define KEY_F10         0xCB
#define KEY_F11         0xCC
#define KEY_F12         0xCD
#define KEY_F13         0xF0
#define KEY_F14         0xF1
#define KEY_F15         0xF2
#define KEY_F16         0xF3
#define KEY_F17         0xF4
#define KEY_F18         0xF5
#define KEY_F19         0xF6
#define KEY_F20         0xF7
#define KEY_F21         0xF8
#define KEY_F22         0xF9
#define KEY_F23         0xFA
#define KEY_F24         0xFB

//旋钮点按功能菜单处理函数
void kpf_proc()
{
  list_show(kpf_menu, M_KPF);
  if (btn.pressed) { btn.pressed = false; switch (btn.id) { case BTN_ID_CW: case BTN_ID_CC: list_rotate_switch(); break;  case BTN_ID_LP: ui.select[ui.layer] = 0; case BTN_ID_SP: switch (ui.select[ui.layer]) {
    
        case 0:   ui.index = M_KNOB;  ui.state = S_LAYER_OUT; break;
        case 1:   break;
        case 2:   check_box_s_select(KNOB_DISABLE, ui.select[ui.layer]); break;
        case 3:   break;
        case 4:   check_box_s_select('A', ui.select[ui.layer]); break;
        case 5:   check_box_s_select('B', ui.select[ui.layer]); break;
        case 6:   check_box_s_select('C', ui.select[ui.layer]); break;
        case 7:   check_box_s_select('D', ui.select[ui.layer]); break;
        case 8:   check_box_s_select('E', ui.select[ui.layer]); break;
        case 9:   check_box_s_select('F', ui.select[ui.layer]); break;
        case 10:  check_box_s_select('G', ui.select[ui.layer]); break;
        case 11:  check_box_s_select('H', ui.select[ui.layer]); break;
        case 12:  check_box_s_select('I', ui.select[ui.layer]); break;
        case 13:  check_box_s_select('J', ui.select[ui.layer]); break;
        case 14:  check_box_s_select('K', ui.select[ui.layer]); break;
        case 15:  check_box_s_select('L', ui.select[ui.layer]); break;
        case 16:  check_box_s_select('M', ui.select[ui.layer]); break;
        case 17:  check_box_s_select('N', ui.select[ui.layer]); break;
        case 18:  check_box_s_select('O', ui.select[ui.layer]); break;
        case 19:  check_box_s_select('P', ui.select[ui.layer]); break;
        case 20:  check_box_s_select('Q', ui.select[ui.layer]); break;
        case 21:  check_box_s_select('R', ui.select[ui.layer]); break;
        case 22:  check_box_s_select('S', ui.select[ui.layer]); break;
        case 23:  check_box_s_select('T', ui.select[ui.layer]); break;
        case 24:  check_box_s_select('U', ui.select[ui.layer]); break;
        case 25:  check_box_s_select('V', ui.select[ui.layer]); break;
        case 26:  check_box_s_select('W', ui.select[ui.layer]); break;
        case 27:  check_box_s_select('X', ui.select[ui.layer]); break;
        case 28:  check_box_s_select('Y', ui.select[ui.layer]); break;
        case 29:  check_box_s_select('Z', ui.select[ui.layer]); break;
        case 30:  break;
        case 31:  check_box_s_select('0', ui.select[ui.layer]); break;
        case 32:  check_box_s_select('1', ui.select[ui.layer]); break;
        case 33:  check_box_s_select('2', ui.select[ui.layer]); break;
        case 34:  check_box_s_select('3', ui.select[ui.layer]); break;
        case 35:  check_box_s_select('4', ui.select[ui.layer]); break;
        case 36:  check_box_s_select('5', ui.select[ui.layer]); break;
        case 37:  check_box_s_select('6', ui.select[ui.layer]); break;
        case 38:  check_box_s_select('7', ui.select[ui.layer]); break;
        case 39:  check_box_s_select('8', ui.select[ui.layer]); break;
        case 40:  check_box_s_select('9', ui.select[ui.layer]); break;
        case 41:  break;
        case 42:  check_box_s_select( KEY_ESC, ui.select[ui.layer]); break;
        case 43:  check_box_s_select( KEY_F1,  ui.select[ui.layer]); break;
        case 44:  check_box_s_select( KEY_F2,  ui.select[ui.layer]); break;
        case 45:  check_box_s_select( KEY_F3,  ui.select[ui.layer]); break;
        case 46:  check_box_s_select( KEY_F4,  ui.select[ui.layer]); break;
        case 47:  check_box_s_select( KEY_F5,  ui.select[ui.layer]); break;
        case 48:  check_box_s_select( KEY_F6,  ui.select[ui.layer]); break;
        case 49:  check_box_s_select( KEY_F7,  ui.select[ui.layer]); break;
        case 50:  check_box_s_select( KEY_F8,  ui.select[ui.layer]); break;
        case 51:  check_box_s_select( KEY_F9,  ui.select[ui.layer]); break;
        case 52:  check_box_s_select( KEY_F10, ui.select[ui.layer]); break;
        case 53:  check_box_s_select( KEY_F11, ui.select[ui.layer]); break;
        case 54:  check_box_s_select( KEY_F12, ui.select[ui.layer]); break;
        case 55:  break;
        case 56:  check_box_s_select( KEY_LEFT_CTRL,   ui.select[ui.layer]); break;
        case 57:  check_box_s_select( KEY_LEFT_SHIFT,  ui.select[ui.layer]); break;
        case 58:  check_box_s_select( KEY_LEFT_ALT,    ui.select[ui.layer]); break;
        case 59:  check_box_s_select( KEY_LEFT_GUI,    ui.select[ui.layer]); break;
        case 60:  check_box_s_select( KEY_RIGHT_CTRL,  ui.select[ui.layer]); break;
        case 61:  check_box_s_select( KEY_RIGHT_SHIFT, ui.select[ui.layer]); break;
        case 62:  check_box_s_select( KEY_RIGHT_ALT,   ui.select[ui.layer]); break;
        case 63:  check_box_s_select( KEY_RIGHT_GUI,   ui.select[ui.layer]); break;
        case 64:  break;
        case 65:  check_box_s_select( KEY_CAPS_LOCK,   ui.select[ui.layer]); break;
        case 66:  check_box_s_select( KEY_BACKSPACE,   ui.select[ui.layer]); break;
        case 67:  check_box_s_select( KEY_RETURN,      ui.select[ui.layer]); break;
        case 68:  check_box_s_select( KEY_INSERT,      ui.select[ui.layer]); break;
        case 69:  check_box_s_select( KEY_DELETE,      ui.select[ui.layer]); break;
        case 70:  check_box_s_select( KEY_TAB,         ui.select[ui.layer]); break;
        case 71:  break;
        case 72:  check_box_s_select( KEY_HOME,        ui.select[ui.layer]); break;
        case 73:  check_box_s_select( KEY_END,         ui.select[ui.layer]); break;
        case 74:  check_box_s_select( KEY_PAGE_UP,     ui.select[ui.layer]); break;
        case 75:  check_box_s_select( KEY_PAGE_DOWN,   ui.select[ui.layer]); break;
        case 76:  break;
        case 77:  check_box_s_select( KEY_UP_ARROW,    ui.select[ui.layer]); break;
        case 78:  check_box_s_select( KEY_DOWN_ARROW,  ui.select[ui.layer]); break;
        case 79:  check_box_s_select( KEY_LEFT_ARROW,  ui.select[ui.layer]); break;
        case 80:  check_box_s_select( KEY_RIGHT_ARROW, ui.select[ui.layer]); break;
        case 81:  break;
      }
    }
  }
}

//电压测量页处理函数
void volt_proc()
{
  volt_show();
  if (btn.pressed) { btn.pressed = false; switch (btn.id) { case BTN_ID_CW: case BTN_ID_CC: list_rotate_switch(); break;

      case BTN_ID_SP: case BTN_ID_LP: ui.index = M_MAIN;  ui.state = S_LAYER_OUT; break;
    }
  }
}

//设置菜单处理函数,多选框列表类模板,弹窗模板
void setting_proc()
{
  list_show(setting_menu, M_SETTING);
  if (btn.pressed) { btn.pressed = false; switch (btn.id) { case BTN_ID_CW: case BTN_ID_CC: list_rotate_switch(); break; case BTN_ID_LP: ui.select[ui.layer] = 0; case BTN_ID_SP: switch (ui.select[ui.layer]) {
        
        //返回更浅层级,长按被当作选择这一项,也是执行这一行
        case 0:   ui.index = M_MAIN;  ui.state = S_LAYER_OUT; break;
        
        //弹出窗口,参数初始化:标题,参数名,参数值,最大值,最小值,步长,背景列表名,背景列表标签
        case 1:   window_value_init("Disp Bri", DISP_BRI, &ui.param[DISP_BRI],  255,  0,  5, setting_menu, M_SETTING);  break;
        case 2:   window_value_init("Tile Ani", TILE_ANI, &ui.param[TILE_ANI],  100, 10,  1, setting_menu, M_SETTING);  break;
        case 3:   window_value_init("List Ani", LIST_ANI, &ui.param[LIST_ANI],  100, 10,  1, setting_menu, M_SETTING);  break;
        case 4:   window_value_init("Win Ani",  WIN_ANI,  &ui.param[WIN_ANI],   100, 10,  1, setting_menu, M_SETTING);  break;
        case 5:   window_value_init("Spot Ani", SPOT_ANI, &ui.param[SPOT_ANI],  100, 10,  1, setting_menu, M_SETTING);  break;
        case 6:   window_value_init("Tag Ani",  TAG_ANI,  &ui.param[TAG_ANI],   100, 10,  1, setting_menu, M_SETTING);  break;
        case 7:   window_value_init("Fade Ani", FADE_ANI, &ui.param[FADE_ANI],  255,  0,  1, setting_menu, M_SETTING);  break;
        case 8:   window_value_init("Btn SPT",  BTN_SPT,  &ui.param[BTN_SPT],   255,  0,  1, setting_menu, M_SETTING);  break;
        case 9:   window_value_init("Btn LPT",  BTN_LPT,  &ui.param[BTN_LPT],   255,  0,  1, setting_menu, M_SETTING);  break;

        //多选框
        case 10:  check_box_m_select( TILE_UFD  );  break;
        case 11:  check_box_m_select( LIST_UFD  );  break;
        case 12:  check_box_m_select( TILE_LOOP );  break;
        case 13:  check_box_m_select( LIST_LOOP );  break;
        case 14:  check_box_m_select( WIN_BOK   );  break;
        case 15:  check_box_m_select( KNOB_DIR  );  break;
        case 16:  check_box_m_select( DARK_MODE );  break;

        //关于本机
        case 17:  ui.index = M_ABOUT; ui.state = S_LAYER_IN; break;
      }
    }
  }
}

//关于本机页
void about_proc()
{
  list_show(about_menu, M_ABOUT);
  if (btn.pressed) { btn.pressed = false; switch (btn.id) { case BTN_ID_CW: case BTN_ID_CC: list_rotate_switch(); break; case BTN_ID_LP: ui.select[ui.layer] = 0; case BTN_ID_SP: switch (ui.select[ui.layer]) {

        case 0:   ui.index = M_SETTING;  ui.state = S_LAYER_OUT; break;
      }
    }
  }
}

//总的UI进程
void ui_proc()
{
  //u8g2.sendBuffer();
  //u8g2_SendBuffer(&u8g2);
  //u8g2_FirstPage(&u8g2);
  switch (ui.state)
  {
    case S_FADE:          fade();                   break;  //转场动画
    case S_WINDOW:        window_param_init();      break;  //弹窗初始化
    case S_LAYER_IN:      layer_init_in();          break;  //层级初始化
    case S_LAYER_OUT:     layer_init_out();         break;  //层级初始化
  
    case S_NONE: u8g2_ClearBuffer(&u8g2);//u8g2.clearBuffer(); 
	switch (ui.index)      //直接选择页面
    {
      case M_WINDOW:      window_proc();            break;
      case M_SLEEP:       sleep_proc();             break;
      case M_MAIN:        main_proc();              break;
      case M_EDITOR:      editor_proc();            break;
      case M_KNOB:        knob_proc();              break;
      case M_KRF:         krf_proc();               break;
      case M_KPF:         kpf_proc();               break;
      case M_VOLT:        volt_proc();              break;
      case M_SETTING:     setting_proc();           break;
      case M_ABOUT:       about_proc();             break;
    }
  }
  u8g2_NextPage(&u8g2);
}

//OLED初始化函数
void oled_init()
{
  //u8g2.setBusClock(1000000);  //硬件IIC接口使用
  //u8g2.begin();
  //u8g2.setContrast(ui.param[DISP_BRI]);
  
  u8g2_SetContrast(&u8g2, ui.param[DISP_BRI]);
  
  //buf_ptr = u8g2.getBufferPtr();
  //buf_len = 8 * u8g2.getBufferTileHeight() * u8g2.getBufferTileWidth();

  buf_ptr = u8g2_GetBufferPtr(&u8g2);
  buf_len = 8 * u8g2_GetBufferTileHeight(&u8g2) * u8g2_GetBufferTileWidth(&u8g2);
}

void flex_ui_setup() 
{
  eeprom_init();
  ui_init();
  oled_init();
  btn_init();

  ESP_LOGI(TAG, "flex ui setup!");
}

void flex_ui_loop() 
{
  btn_scan();
  knob_inter();
  ui_proc();
}



~小结:

         这次主要记录了WouoUI框架在esp32 idf环境下的移植,WouoUI的框架原本是在arduino下面实现的,最终用丙正正实现了移植,移植过程主要难点在于arduino框架u8g2类各种方法的丙语言实现替换,其中又属print重载函数的实现最麻烦,还好最终一一克服,将这优雅的UI呈现在小黑上面;

以下是一些和本文相关的文章链接和U8g2的wiki链接:

一、~~呆萌的瓦力平衡机器人~~链接:

        1.基于ESP32双无刷FOC电机的瓦力平衡机器人(1)

        2.基于ESP32双无刷FOC电机的瓦力平衡机器人(2)

二、 ~~U8g2图形库使用技巧记录~~链接:

        1.U8g2图形库使用技巧记录(1)

        2.U8g2图形库使用技巧记录(2)

        3.U8g2图形库使用技巧记录(3)

三、U8g2 wiki链接:

        1.U8g2_wiki

        2.WouoUI框架,可以直接搜索;

后续的计划:

       目前UI的大体框架已经定型,后续会基于该框架实现小车参数的设置和增加一些动效页面的;          

                      大家如果也感兴趣,可以来这交流学习(这里提供了丰富的esp32资料):

                             

                                                        燥起来吧!!!!!! 

  • 2
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
您好!关于u8g2在STM32上的使用,以下是一般的步骤: 1. 首先,确保您的STM32开发板已连接到您的计算机,并且您已经安装了适当的开发环境(如Keil、STM32CubeIDE等)。 2. 下载u8g2的最新版本,可以从u8g2官方网站(https://github.com/olikraus/u8g2)上找到源代码。 3. 将下载的文件解压缩,并将u8g2文件夹复制到您的项目目录中。 4. 打开您的开发环境,并创建一个新的STM32项目。 5. 在您的项目中包含u8g2的头文件。根据您的开发环境不同,可能有不同的方法。例如,在Keil中,您可以通过右键单击项目->Options for Target->C/C++选项卡->Include Paths and Symbols来添加路径。 6. 在您的代码中,使用合适的接口初始化u8g2。根据您使用的显示设备类型(如OLED、LCD等),选择适当的初始化函数。例如,如果您使用I2C接口的OLED显示屏,可以使用以下代码进行初始化: ```c #include "u8g2.h" u8g2_t u8g2; void u8g2_init(void) { u8g2_Setup_ssd1306_i2c_128x64_noname_f(&u8g2, U8G2_R0, u8x8_byte_sw_i2c, u8x8_gpio_and_delay_stm32cube_hal); u8x8_SetI2CAddress(&u8g2.u8x8, 0x78); u8g2_InitDisplay(&u8g2); u8g2_SetPowerSave(&u8g2, 0); } ``` 7. 使用u8g2提供的函数来控制显示设备。例如,您可以使用以下代码在屏幕上绘制文本: ```c u8g2_DrawStr(&u8g2, 0, 10, "Hello, World!"); u8g2_SendBuffer(&u8g2); ``` 这是一个简单的示例,演示了如何在STM32上使用u8g2。您可以根据自己的需求使用其他u8g2的功能和函数来绘制图形、显示图像等。 希望这可以帮助到您!如有其他问题,请随时提问。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值