diff --git a/WouoUI-128_128/WouoUI-128_128.ino b/WouoUI-128_128/WouoUI-128_128.ino new file mode 100644 index 0000000..69b0089 --- /dev/null +++ b/WouoUI-128_128/WouoUI-128_128.ino @@ -0,0 +1,1741 @@ + +/* + 此项目模仿自稚晖君暂未开源的 MonoUI,用于实现类似 UltraLink 的丝滑界面 + + 有四个版本: + + * 分辨率 128 * 128 :主菜单,列表和关于本机页基本还原了 UltraLink 的界面和动态效果,电压测量页为原创设计 + * 分辨率 128 * 64 :主菜单模仿 UltraLink 重新设计,去掉了小标题,电压测量页重新设计,关于本机页面改为列表,列表适配了该分辨率 + * 分辨率 128 * 32 :在 128 * 64 分辨率的基础上,主界面只保留图标,电压测量页重新设计 + * 通用版本:仅保留列表,主菜单也改为列表,删除电压测量页和与列表无关的动画(保留弹窗效果),经过简单修改可以适配任何分辨率,任何行高度的情况 + + WouoUI v2.0 功能: + + * 全部使用非线性的平滑缓动动画,包括列表,弹窗,甚至进度条 + * 优化平滑动画算法到只有两行,分类别定义平滑权重,并且每个权重值都分别可调 + * 可以打断的非线性动画,当前动画未结束但下一次动画已经被触发时,动画可以自然过渡 + * 非通用版本分别适配了类似 UltraLink 主菜单的磁贴界面(因为让我想起 WP7 的 Metron 风格,所以称之为磁贴) + * 通用版本仅保留列表类界面,经过简单修改可以适配所有分辨率的屏幕,包括屏幕内行数不是整数的情况 + * 列表菜单,列表可以无限延长 + * 列表文字选择框,选择框可根据选择的字符串长度自动伸缩,进入菜单时从列表开头从长度 0 展开,转到上一级列表时,长度和纵坐标平滑移动到上一级选择的位置 + * 列表单选框,储存数据时也储存该值在列表中所在的位置,展开列表时根据每行开头的字符判断是否绘制外框,再根据位置数据判断是否绘制里面的点 + * 列表多选框,储存数据的数组跟多选框列表的行数对应,不要求连续排列,展开列表时根据每行开头的字符判断是否绘制外框,再根据行数对应的储存数据位置的数值是否为1判断是否绘制里面的点 + * 列表显示数值,与多选框原理相似,但不涉及修改操作 + * 列表展开动画,初始化列表时,可以选择列表从头开始展开,或者从上次选中的位置展开 + * 图标展开动画,初始化磁贴类界面时,可以选择图标从头开始展开,或者从上次选中的位置展开 + * 弹出窗口,实现了窗口弹出的动画效果,可以自定义最大值,最小值,步进值,需要修改的参数等,窗口独立运行,调用非常简单 + * 弹出窗口背景虚化可选项,背景虚化会产生卡顿感,但删掉代码有些可惜,因此做成可选项,默认关闭 + * 亮度调节,在弹出窗口中调节亮度值可以实时改变当前亮度值 + * 旋钮功能,使用EC11旋钮控制,旋钮方向可以软件调整,内置一个USB控制电脑的示例,在睡眠模式下旋转调整音量或者亮度,短按输入一个键值,长按进入主菜单,旋钮消抖时长等参数可以在弹出窗口中调整 + * 循环模式,选择项超过头尾时,选择项跳转到另一侧继续,列表和磁贴类可以分别选择 + * 黑暗模式,其实本来就是黑暗模式,是增加了白天模式,默认开启黑暗模式 + * 消失动画适配两种模式,一种是渐变成全黑,另一种渐变成全白 + * 断电存储,用简单直观的方式将每种功能参数写入EEPROM,只在修改过参数,进入睡眠模式时写入,避免重复擦写,初始化时检查11个标志位,允许一位误码 + + 项目参考: + + * B站:路徍要什么自行车:在线仿真:https://wokwi.com/projects/350306511434547796,https://www.bilibili.com/video/BV1HA411S7pv/ + * Github:createskyblue:OpenT12:https://github.com/createskyblue/OpenT12 + * Github:peng-zhihui:OpenHeat:https://github.com/peng-zhihui/OpenHeat + + 注意事项: + + * 示例使用的单片机为 STM32F103C8T6 ,旋钮为 EC11 ,旋钮旋转使用中断程序,旋钮质量不好可能使界面卡死,原因不明,如果能查明原因请联系我 + * 为防止使用者在上传程序后无法直接使用,认为是代码有问题,HID功能默认禁用,如需使用旋钮音量控制和点按输入功能,请在初始化函数中启用相关功能,上传成功后设置两个跳线帽都为0,断电再插上 USB 线 + + 本项目使用Apache 2.0开源协议,如需商用或借鉴,请在醒目处标注本项目开源地址 + 欢迎关注我的B站账号,用户名:音游玩的人,B站主页:https://space.bilibili.com/9182439?spm_id_from=..0.0 +*/ + +/************************************* 屏幕驱动 *************************************/ + +//分辨率128*128,建议使用硬件SPI接口 + +#include +#include + +#define SCL PA5 +#define SDA PA7 +#define RES U8X8_PIN_NONE +#define DC PB6 +#define CS PB7 + +U8G2_SH1107_SEEED_128X128_F_4W_HW_SPI u8g2(U8G2_R0, CS, DC, RES); + +/************************************* 定义页面 *************************************/ + +//总目录,缩进表示页面层级 +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 main_menu_exp[] +{ + {"[ Activate Func ]"}, + {"[ Modify Func ]"}, + {"[ View Voltages ]"}, + {"[ Modify Config ]"}, +}; + +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[] +{ + {"- PA0"}, + {"- PA1"}, + {"- PA2"}, + {"- PA3"}, + {"- PA4"}, + {"- PA5"}, + {"- PA6"}, + {"- PA7"}, + {"- PB0"}, + {"- PB1"}, +}; + +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[] +{ + {"WouoUI"}, + {"v2.0"}, + {"Board: STM32F103"}, + {"Ram: 20k"}, + {"Flash: 64k"}, + {"Freq: 72Mhz"}, + {"Creator: RQNG"}, + {"Billi UID: 9182439"}, +}; + +/************************************* 图片内容 *************************************/ + +PROGMEM const uint8_t main_icon_pic[][16*18] +{ + { + 0xF8,0xFF,0xFF,0xFF,0xFF,0x1F,0xFE,0xFF,0xFF,0xFF,0xFF,0x7F,0xFE,0xFF,0xFF,0xFF, + 0xFF,0x7F,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x7F,0xE0,0xFF,0xFF,0xFF,0xFF, + 0xFF,0x01,0xFF,0xFF,0xFF,0xFF,0xFF,0x07,0xFC,0xFF,0xFF,0xFF,0xFF,0x0F,0xF8,0xFF, + 0xFF,0xFF,0xFF,0x1F,0xE0,0xFF,0xFF,0xFF,0xFF,0x3F,0xC0,0xFF,0xFF,0xFF,0xFF,0x7F, + 0x80,0xFF,0xFF,0xFF,0xFF,0x7F,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0x00,0xFF,0xFF,0xFF, + 0xFF,0xFF,0x00,0xFE,0xFF,0xFF,0xFF,0xFF,0x00,0xFC,0xFF,0xFF,0xFF,0xFF,0x01,0xFC, + 0xFF,0xFF,0xFF,0xFF,0x01,0xF8,0xFF,0xFF,0xFF,0xFF,0x01,0xF8,0xFF,0xFF,0xFF,0xFF, + 0x01,0xF8,0xFF,0xFF,0xFF,0xFF,0x01,0xF0,0xFF,0xFF,0xFF,0xFF,0x01,0xF0,0xFF,0xFF, + 0xFF,0xFF,0x01,0xF0,0xFF,0xFF,0xFF,0xFF,0x00,0xF0,0xEF,0xFF,0xFF,0xFF,0x00,0xF0, + 0xEF,0xFF,0xFF,0xFF,0x00,0xF0,0xCF,0xFF,0xFF,0x7F,0x00,0xF0,0xCF,0xFF,0xFF,0x7F, + 0x00,0xF0,0x8F,0xFF,0xFF,0x3F,0x00,0xF0,0x0F,0xFF,0xFF,0x1F,0x00,0xF0,0x1F,0xFE, + 0xFF,0x0F,0x00,0xF8,0x1F,0xFC,0xFF,0x07,0x00,0xF8,0x1F,0xF0,0xFF,0x01,0x00,0xF8, + 0x3F,0x80,0x3F,0x00,0x00,0xFC,0x3F,0x00,0x00,0x00,0x00,0xFC,0x7F,0x00,0x00,0x00, + 0x00,0xFE,0xFF,0x00,0x00,0x00,0x00,0xFF,0xFF,0x00,0x00,0x00,0x00,0xFF,0xFF,0x01, + 0x00,0x00,0x80,0xFF,0xFF,0x03,0x00,0x00,0xC0,0xFF,0xFF,0x07,0x00,0x00,0xE0,0xFF, + 0xFF,0x1F,0x00,0x00,0xF8,0xFF,0xFF,0x3F,0x00,0x00,0xFC,0xFF,0xFF,0xFF,0x00,0x00, + 0xFF,0xFF,0xFF,0xFF,0x07,0xE0,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFE,0xFF, + 0xFF,0xFF,0xFF,0x7F,0xFE,0xFF,0xFF,0xFF,0xFF,0x7F,0xF8,0xFF,0xFF,0xFF,0xFF,0x1F, + }, + { + 0xF8,0xFF,0xFF,0xFF,0xFF,0x1F,0xFE,0xFF,0xFF,0xFF,0xFF,0x7F,0xFE,0xFF,0xFF,0xFF, + 0xFF,0x7F,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xF9,0x7F,0xFE,0x9F,0xFF,0xFF,0xF9, + 0x7F,0xFE,0x9F,0xFF,0xFF,0xF9,0x7F,0xFE,0x9F,0xFF,0xFF,0xF9,0x7F,0xFE,0x9F,0xFF, + 0xFF,0xF9,0x7F,0xFE,0x9F,0xFF,0xFF,0xF9,0x7F,0xFE,0x9F,0xFF,0xFF,0xF9,0x7F,0xFE, + 0x9F,0xFF,0xFF,0xF9,0x3F,0xFC,0x9F,0xFF,0xFF,0xF9,0x1F,0xF8,0x9F,0xFF,0xFF,0xF9, + 0x1F,0xF8,0x9F,0xFF,0xFF,0xF0,0x1F,0xF8,0x0F,0xFF,0xFF,0xF0,0x1F,0xF8,0x0F,0xFF, + 0xFF,0xF9,0x1F,0xF8,0x9F,0xFF,0xFF,0xF9,0x1F,0xF8,0x9F,0xFF,0xFF,0xF9,0x3F,0xFC, + 0x9F,0xFF,0xFF,0xF9,0x7F,0xFE,0x9F,0xFF,0xFF,0xF9,0x7F,0xFE,0x9F,0xFF,0xFF,0xF9, + 0x7F,0xFE,0x9F,0xFF,0xFF,0xF9,0x7F,0xFE,0x9F,0xFF,0xFF,0xF9,0x7F,0xFE,0x9F,0xFF, + 0xFF,0xF9,0x7F,0xFE,0x9F,0xFF,0xFF,0xF9,0x7F,0xFE,0x9F,0xFF,0xFF,0xF9,0x7F,0xFE, + 0x9F,0xFF,0xFF,0xF9,0x7F,0xFE,0x9F,0xFF,0xFF,0xF9,0x7F,0xFE,0x9F,0xFF,0xFF,0xF0, + 0x7F,0xFE,0x0F,0xFF,0x7F,0xE0,0x7F,0xFE,0x07,0xFE,0x7F,0xE0,0x7F,0xFE,0x07,0xFE, + 0x7F,0xE0,0x3F,0xFC,0x07,0xFE,0x7F,0xE0,0x3F,0xFC,0x07,0xFE,0x7F,0xE0,0x7F,0xFE, + 0x07,0xFE,0x7F,0xE0,0x7F,0xFE,0x07,0xFE,0xFF,0xF0,0x7F,0xFE,0x0F,0xFF,0xFF,0xF9, + 0x7F,0xFE,0x9F,0xFF,0xFF,0xF9,0x7F,0xFE,0x9F,0xFF,0xFF,0xF9,0x7F,0xFE,0x9F,0xFF, + 0xFF,0xF9,0x7F,0xFE,0x9F,0xFF,0xFF,0xF9,0x7F,0xFE,0x9F,0xFF,0xFF,0xF9,0x7F,0xFE, + 0x9F,0xFF,0xFF,0xF9,0x7F,0xFE,0x9F,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFE,0xFF, + 0xFF,0xFF,0xFF,0x7F,0xFE,0xFF,0xFF,0xFF,0xFF,0x7F,0xF8,0xFF,0xFF,0xFF,0xFF,0x1F, + }, + { + 0xF8,0xFF,0xFF,0xFF,0xFF,0x1F,0xFE,0xFF,0xFF,0xFF,0xFF,0x7F,0xFE,0xFF,0xFF,0xFF, + 0xFF,0x7F,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFD, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xF8,0xFF,0xFF,0xFF,0xFF,0xFF,0xF8,0xFF,0xFF,0xFF,0xFF, + 0x7F,0xF0,0xFF,0xFF,0xFF,0xFF,0x7F,0xF0,0xFF,0xFF,0xFF,0xFF,0x3F,0xE0,0xFF,0xFF, + 0xFF,0xFF,0x3F,0xE0,0xC1,0xFF,0xFF,0xFF,0xFF,0xF8,0x80,0xFF,0xFF,0xFF,0xFF,0x78, + 0x00,0xFF,0xFF,0xFF,0xFF,0x38,0x00,0xFE,0xFF,0xFF,0xFF,0x38,0x1C,0xFE,0xFF,0xFF, + 0xFF,0x18,0x3E,0xFC,0xFF,0xFF,0xFF,0x18,0x7F,0xFC,0xFF,0xFF,0xFF,0x08,0x7F,0xF8, + 0xFF,0xFB,0xFF,0x88,0xFF,0xF8,0xFF,0xFB,0xFF,0x80,0xFF,0xF0,0xFF,0xF9,0xFF,0xC0, + 0xFF,0xF1,0xFF,0xF9,0xFF,0xC0,0xFF,0xF1,0xFF,0xF9,0xFF,0xC0,0xFF,0xE1,0xFF,0xF8, + 0xFF,0xE0,0xFF,0xE3,0xFF,0xF8,0xFF,0xE0,0xFF,0xC3,0x7F,0xF8,0xFF,0xF0,0xFF,0xC7, + 0x7F,0xFC,0xFF,0xF0,0xFF,0xC7,0x7F,0xFC,0xFF,0xF0,0xFF,0x87,0x3F,0xFC,0xFF,0xF8, + 0xFF,0x8F,0x3F,0xFE,0xFF,0xF8,0xFF,0x0F,0x1F,0xFE,0xFF,0xF8,0xFF,0x1F,0x0E,0xFF, + 0xFF,0xF8,0xFF,0x1F,0x00,0xFF,0xFF,0xF8,0xFF,0x3F,0x80,0xFF,0xFF,0xF8,0xFF,0x3F, + 0xC0,0xFF,0xFF,0xF8,0xFF,0xFF,0xE0,0xFF,0xFF,0xF8,0xFF,0xFF,0xFF,0xFF,0xFF,0xF8, + 0xFF,0xFF,0xCF,0xFF,0xFF,0xF8,0xFF,0xFF,0x0F,0xFF,0xFF,0x00,0x00,0x00,0x00,0xFC, + 0xFF,0x00,0x00,0x00,0x00,0xF8,0xFF,0x00,0x00,0x00,0x00,0xFC,0xFF,0xFF,0xFF,0xFF, + 0x0F,0xFF,0xFF,0xFF,0xFF,0xFF,0xCF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFE,0xFF, + 0xFF,0xFF,0xFF,0x7F,0xFE,0xFF,0xFF,0xFF,0xFF,0x7F,0xF8,0xFF,0xFF,0xFF,0xFF,0x1F, + }, + { + 0xF8,0xFF,0xFF,0xFF,0xFF,0x1F,0xFE,0xFF,0xFF,0xFF,0xFF,0x7F,0xFE,0xFF,0xFF,0xFF, + 0xFF,0x7F,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x3F,0xFC,0xFF,0xFF,0xFF,0xFF, + 0x1F,0xF8,0xFF,0xFF,0xFF,0xFF,0x1F,0xF8,0xFF,0xFF,0xFF,0xFF,0x1F,0xF8,0xFF,0xFF, + 0xFF,0xFF,0x1F,0xF8,0xFF,0xFF,0xFF,0xC7,0x0F,0xF0,0xE3,0xFF,0xFF,0x83,0x07,0xE0, + 0xC1,0xFF,0xFF,0x01,0x00,0x00,0x80,0xFF,0xFF,0x01,0x00,0x00,0x80,0xFF,0xFF,0x01, + 0x00,0x00,0x80,0xFF,0xFF,0x03,0x00,0x00,0xC0,0xFF,0xFF,0x07,0x00,0x00,0xE0,0xFF, + 0xFF,0x07,0xE0,0x07,0xE0,0xFF,0xFF,0x07,0xF8,0x1F,0xE0,0xFF,0xFF,0x07,0xFC,0x3F, + 0xE0,0xFF,0xFF,0x03,0xFE,0x7F,0xC0,0xFF,0xFF,0x01,0xFE,0x7F,0x80,0xFF,0x1F,0x00, + 0xFF,0xFF,0x00,0xF8,0x0F,0x00,0xFF,0xFF,0x00,0xF0,0x0F,0x00,0xFF,0xFF,0x00,0xF0, + 0x0F,0x00,0xFF,0xFF,0x00,0xF0,0x0F,0x00,0xFF,0xFF,0x00,0xF0,0x1F,0x00,0xFF,0xFF, + 0x00,0xF8,0xFF,0x03,0xFE,0x7F,0xC0,0xFF,0xFF,0x03,0xFE,0x7F,0xC0,0xFF,0xFF,0x07, + 0xFC,0x3F,0xE0,0xFF,0xFF,0x07,0xF8,0x1F,0xE0,0xFF,0xFF,0x07,0xE0,0x07,0xE0,0xFF, + 0xFF,0x07,0x00,0x00,0xE0,0xFF,0xFF,0x03,0x00,0x00,0xC0,0xFF,0xFF,0x01,0x00,0x00, + 0x80,0xFF,0xFF,0x01,0x00,0x00,0x80,0xFF,0xFF,0x01,0x00,0x00,0x80,0xFF,0xFF,0x83, + 0x07,0xE0,0xC1,0xFF,0xFF,0xC7,0x0F,0xF0,0xE3,0xFF,0xFF,0xFF,0x1F,0xF8,0xFF,0xFF, + 0xFF,0xFF,0x1F,0xF8,0xFF,0xFF,0xFF,0xFF,0x1F,0xF8,0xFF,0xFF,0xFF,0xFF,0x1F,0xF8, + 0xFF,0xFF,0xFF,0xFF,0x3F,0xFC,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFE,0xFF, + 0xFF,0xFF,0xFF,0x7F,0xFE,0xFF,0xFF,0xFF,0xFF,0x7F,0xF8,0xFF,0xFF,0xFF,0xFF,0x1F, + }, +}; + +/************************************* 页面变量 *************************************/ + +//OLED变量 +#define DISP_H 128 //屏幕高度 +#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_SLEEP; + uint8_t state = S_NONE; + bool sleep = true; + uint8_t fade = 1; + uint8_t param[UI_PARAM]; +} ui; + +//磁贴变量 +//所有磁贴页面都使用同一套参数 +#define TILE_B_FONT u8g2_font_helvB24_tr //磁贴大标题字体 +#define TILE_S_FONT u8g2_font_HelvetiPixel_tr //磁贴小标题字体 +#define TILE_B_TITLE_H 25 //磁贴大标题字体高度 +#define TILE_S_TITLE_H 8 //磁贴小标题字体高度 +#define TILE_ICON_H 48 //磁贴图标高度 +#define TILE_ICON_W 48 //磁贴图标宽度 +#define TILE_ICON_S 57 //磁贴图标间距 +#define TILE_INDI_H 40 //磁贴大标题指示器高度 +#define TILE_INDI_W 10 //磁贴大标题指示器宽度 +#define TILE_INDI_S 57 //磁贴大标题指示器上边距 +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; + +//电压测量页面变量 +//开发板模拟引脚 +uint8_t analog_pin[10] = { PA0, PA1, PA2, PA3, PA4, PA5, PA6, PA7, PB0, PB1 }; +//曲线相关 +#define WAVE_SAMPLE 20 //采集倍数 +#define WAVE_W DISP_W //波形宽度 +#define WAVE_L 0 //波形左边距 +#define WAVE_U 0 //波形上边距 +#define WAVE_MAX 43 //最大值 +#define WAVE_MIN 5 //最小值 +#define WAVE_BOX_H 49 //波形边框高度 +#define WAVE_BOX_W DISP_W //波形边框宽度 +//列表和文字背景框相关 +#define VOLT_FONT u8g2_font_helvB24_tr //电压数字字体 +#define VOLT_LIST_U_S 94 //列表上边距 +#define VOLT_TEXT_BG_U_S 53 //文字背景框上边距 +#define VOLT_TEXT_BG_H 33 //文字背景框高度 +struct +{ + int ch0_adc[WAVE_SAMPLE * WAVE_W]; + int ch0_wave[WAVE_W]; + int val; + float text_bg_l; + float text_bg_l_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 ABOUT_FONT u8g2_font_HelvetiPixel_tr //关于本机字体 +#define ABOUT_INDI_S 4 //关于本机页面列表指示左边距,也用于规范页面内元素之间的位置关系 +#define ABOUT_INDI_W 2 //关于本机页面列表指示器宽度 +struct +{ + float indi_x; + float indi_x_trg; +} about; + +/************************************* USB 相关 *************************************/ + +#include + +USBHID HID; + +const uint8_t reportDescription[] = +{ + HID_CONSUMER_REPORT_DESCRIPTOR(), + HID_KEYBOARD_REPORT_DESCRIPTOR() +}; + +HIDConsumer Consumer(HID); +HIDKeyboard Keyboard(HID); + +void hid_init() +{ + HID.begin(reportDescription, sizeof(reportDescription)); + while (!USBComposite); +} + +/********************************** 自定义功能变量 **********************************/ + +//旋钮功能变量 +#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变量 +#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() +{ + 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; +} + +//EEPROM读数据,开机初始化时执行一遍 +void eeprom_read_all_data() +{ + 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; +} + +//开机检查是否已经修改过,没修改过则跳过读配置步骤,用默认设置 +void eeprom_init() +{ + 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(); +} + +/************************************* 旋钮相关 *************************************/ + +//可按下旋钮引脚 +#define AIO PB12 +#define BIO PB13 +#define SW PB14 + +//按键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; +} btn; + +void knob_inter() +{ + 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; + } +} + +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; + } + } +} + +void btn_init() +{ + pinMode(AIO, INPUT); + pinMode(BIO, INPUT); + pinMode(SW, INPUT_PULLUP); + attachInterrupt(digitalPinToInterrupt(AIO), knob_inter, CHANGE); +} + +/************************************ 初始化函数 ***********************************/ + +/********************************* 初始化数据处理函数 *******************************/ + +//显示数值的初始化 +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() +{ + u8g2.setDrawColor(0); + u8g2.drawBox(0, 0, DISP_W, DISP_H); + u8g2.setPowerSave(1); + 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_l = 0; + volt.text_bg_l_trg = DISP_W; +} + +//设置页初始化 +void setting_param_init() +{ + check_box_v_init(ui.param); + check_box_m_init(ui.param); +} + +//关于本机页初始化 +void about_param_init() +{ + about.indi_x = 0; + about.indi_x_trg = ABOUT_INDI_S; +} + +/********************************** 通用初始化函数 *********************************/ + +/* + 页面层级管理逻辑是,把所有页面都先当作列表类初始化,不是列表类按需求再初始化对应函数 + 这样做会浪费一些资源,但跳转页面时只需要考虑页面层级,逻辑上更清晰,减少出错 +*/ + +//弹窗动画初始化 +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; //主菜单进入设置页,单选框初始化 + case M_ABOUT: about_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[], struct MENU arr_2[], const uint8_t icon_pic[][16*18]) +{ + //计算动画过渡值 + 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.setFontDirection(0); + + //绘制大标题 + 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(TILE_S_FONT); + u8g2.drawStr(((DISP_W - u8g2.getStrWidth(arr_2[ui.select[ui.layer]].m_select)) / 2), 0.5 * (TILE_ICON_S + TILE_INDI_H + DISP_H + LIST_TEXT_H), arr_2[ui.select[ui.layer]].m_select); + + //绘制大标题指示器 + u8g2.drawBox(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]); + } + 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.setDrawColor(2); + if (!ui.param[DARK_MODE]) u8g2.drawBox(0, 0, DISP_W, DISP_H); +} + +/*************** 根据列表每行开头符号,判断每行尾部是否绘制以及绘制什么内容 *************/ + +//列表显示数值 +void list_draw_value(int n) { 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); } + +//绘制框里面的点 +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); } + +//列表显示旋钮功能 +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); + 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); + list.box_x_trg = u8g2.getStrWidth(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.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); + + //绘制列表文字 + 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); + + //反转屏幕内元素颜色,白天模式遮罩,在这里屏蔽有列表参与的页面,使遮罩作用在那个页面上 + if (!ui.param[DARK_MODE]) + { + u8g2.drawBox(0, 0, DISP_W, DISP_H); + switch(ui.index) + { + case M_WINDOW: + case M_VOLT: + u8g2.drawBox(0, 0, DISP_W, DISP_H); + } + } +} + +//电压页面显示函数 +void volt_show() +{ + //更新动画目标值 + u8g2.setFont(LIST_FONT); + list.box_x_trg = u8g2.getStrWidth(volt_menu[ui.select[ui.layer]].m_select) + LIST_TEXT_S * 2; + + //计算动画过渡值 + 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(&volt.text_bg_l, &volt.text_bg_l_trg, TAG_ANI); + + //检查循环动画是否结束 + if (list.loop && list.box_y == list.box_y_trg[ui.layer]) list.loop = false; + + //设置文字和曲线颜色,0透显,1实显,2反色,这里都用实显 + u8g2.setDrawColor(1); + + //绘制列表文字 + u8g2.setFontDirection(1); + if (!ui.init) + { + for (uint8_t i = 0; i < ui.num[ui.index]; ++ i) u8g2.drawStr(LIST_TEXT_S + (i - ui.select[ui.layer]) * list.y + list.box_y_trg[ui.layer] - 1, VOLT_LIST_U_S , volt_menu[i].m_select); + 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 (uint8_t i = 0; i < ui.num[ui.index]; ++ i) u8g2.drawStr(LIST_TEXT_S + LIST_LINE_H * i + (int16_t)list.y - 1, VOLT_LIST_U_S , volt_menu[i].m_select); + + //绘制电压曲线和外框 + volt.val = 0; + u8g2.drawFrame(0, 0, WAVE_BOX_W, WAVE_BOX_H); + u8g2.drawFrame(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 = analogRead(analog_pin[ui.select[ui.layer]]); + for (int i = 1; i < WAVE_W - 1; i++) + { + volt.ch0_wave[i] = 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]); + } + } + + //绘制电压值 + u8g2.setFontDirection(0); + u8g2.setFont(VOLT_FONT); + u8g2.setCursor(23, VOLT_LIST_U_S - 12); + u8g2.print(volt.val / 4096.0 * 3.3); + u8g2.print("V"); + + //绘制列表选择框和电压文字背景 + u8g2.setDrawColor(2); + u8g2.drawRBox(list.box_y, VOLT_LIST_U_S - LIST_TEXT_S, LIST_LINE_H, list.box_x, LIST_BOX_R); + u8g2.drawBox(DISP_W - volt.text_bg_l, VOLT_TEXT_BG_U_S, DISP_W, VOLT_TEXT_BG_H); + + //反转屏幕内元素颜色,白天模式遮罩 + if (!ui.param[DARK_MODE]) u8g2.drawBox(0, 0, DISP_W, DISP_H); +} + +//弹窗通用显示函数 +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); + 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); + + //绘制窗口 + 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); +} + +//关于本机显示函数,使用了部分列表类页面参数 +void about_show() +{ + //更新动画目标值 + u8g2.setFont(ABOUT_FONT); + list.box_x_trg = u8g2.getStrWidth(about_menu[0].m_select) + LIST_TEXT_S * 2; + + //计算动画过渡值 + animation(&list.box_x, &list.box_x_trg, TAG_ANI); + animation(&about.indi_x, &about.indi_x_trg, TAG_ANI); + + //设置文字和指示器颜色,0透显,1实显,2反色,这里都用实显 + u8g2.setDrawColor(1); + + //绘制列表 + u8g2.drawStr(ABOUT_INDI_S + LIST_TEXT_S, ABOUT_INDI_S + LIST_TEXT_S + LIST_TEXT_H, about_menu[0].m_select); + u8g2.drawStr(ABOUT_INDI_S + list.box_x_trg + ABOUT_INDI_S, ABOUT_INDI_S + LIST_TEXT_S + LIST_TEXT_H, about_menu[1].m_select); + for (int i = 2 ; i < ui.num[M_ABOUT] ; i++) u8g2.drawStr(about.indi_x_trg + ABOUT_INDI_W + ABOUT_INDI_S * 2, ABOUT_INDI_S + LIST_LINE_H + LIST_TEXT_S / 2 + (i - 1) * LIST_LINE_H, about_menu[i].m_select); + u8g2.drawBox(about.indi_x, ABOUT_INDI_S + LIST_LINE_H + ABOUT_INDI_S * 2, ABOUT_INDI_W, (ui.num[M_ABOUT] - 2) * LIST_LINE_H - LIST_TEXT_S); + + //绘制标题背景框 + u8g2.setDrawColor(2); + u8g2.drawRBox(ABOUT_INDI_S, ABOUT_INDI_S, list.box_x, LIST_LINE_H, LIST_BOX_R); + + //反转屏幕内元素颜色,白天模式遮罩 + if (!ui.param[DARK_MODE]) u8g2.drawBox(0, 0, DISP_W, DISP_H); +} + +/************************************* 处理函数 *************************************/ + +/*********************************** 通用处理函数 ***********************************/ + +//磁贴类页面旋转时判断通用函数 +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_CW: if (*win.value < win.max) *win.value += win.step; eeprom.change = true; break; + case BTN_ID_CC: 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(); + + //当旋钮有动作时 + if (btn.pressed) { btn.pressed = false; switch (btn.id) { + + //睡眠时顺时针旋转执行的函数 + 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; + + //睡眠时长按执行的函数 + case BTN_ID_LP: ui.index = M_MAIN; ui.state = S_LAYER_IN; u8g2.setPowerSave(0); ui.sleep = false; break; + } + } + } +} + +//主菜单处理函数,磁贴类模板 +void main_proc() +{ + tile_show(main_menu, main_menu_exp, 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; + } + } + } +} + +//旋钮点按功能菜单处理函数 +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() +{ + about_show(); + if (btn.pressed) { btn.pressed = false; switch (btn.id) { + + case BTN_ID_SP: case BTN_ID_LP: ui.index = M_SETTING; ui.state = S_LAYER_OUT; break; + } + } +} + +//总的UI进程 +void ui_proc() +{ + u8g2.sendBuffer(); + 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(); 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; + } + } +} + +//OLED初始化函数 +void oled_init() +{ + u8g2.setBusClock(10000000); //硬件SPI接口使用 + u8g2.begin(); + u8g2.setContrast(ui.param[DISP_BRI]); + buf_ptr = u8g2.getBufferPtr(); + buf_len = 8 * u8g2.getBufferTileHeight() * u8g2.getBufferTileWidth(); +} + +void setup() +{ + eeprom_init(); + ui_init(); + oled_init(); + btn_init(); + //hid_init(); +} + +void loop() +{ + btn_scan(); + ui_proc(); +} + + diff --git a/WouoUI-128_32/WouoUI-128_32.ino b/WouoUI-128_32/WouoUI-128_32.ino new file mode 100644 index 0000000..4e82378 --- /dev/null +++ b/WouoUI-128_32/WouoUI-128_32.ino @@ -0,0 +1,1596 @@ + +/* + 此项目模仿自稚晖君暂未开源的 MonoUI,用于实现类似 UltraLink 的丝滑界面 + + 有四个版本: + + * 分辨率 128 * 128 :主菜单,列表和关于本机页基本还原了 UltraLink 的界面和动态效果,电压测量页为原创设计 + * 分辨率 128 * 64 :主菜单模仿 UltraLink 重新设计,去掉了小标题,电压测量页重新设计,关于本机页面改为列表,列表适配了该分辨率 + * 分辨率 128 * 32 :在 128 * 64 分辨率的基础上,主界面只保留图标,电压测量页重新设计 + * 通用版本:仅保留列表,主菜单也改为列表,删除电压测量页和与列表无关的动画(保留弹窗效果),经过简单修改可以适配任何分辨率,任何行高度的情况 + + WouoUI v2.0 功能: + + * 全部使用非线性的平滑缓动动画,包括列表,弹窗,甚至进度条 + * 优化平滑动画算法到只有两行,分类别定义平滑权重,并且每个权重值都分别可调 + * 可以打断的非线性动画,当前动画未结束但下一次动画已经被触发时,动画可以自然过渡 + * 非通用版本分别适配了类似 UltraLink 主菜单的磁贴界面(因为让我想起 WP7 的 Metron 风格,所以称之为磁贴) + * 通用版本仅保留列表类界面,经过简单修改可以适配所有分辨率的屏幕,包括屏幕内行数不是整数的情况 + * 列表菜单,列表可以无限延长 + * 列表文字选择框,选择框可根据选择的字符串长度自动伸缩,进入菜单时从列表开头从长度 0 展开,转到上一级列表时,长度和纵坐标平滑移动到上一级选择的位置 + * 列表单选框,储存数据时也储存该值在列表中所在的位置,展开列表时根据每行开头的字符判断是否绘制外框,再根据位置数据判断是否绘制里面的点 + * 列表多选框,储存数据的数组跟多选框列表的行数对应,不要求连续排列,展开列表时根据每行开头的字符判断是否绘制外框,再根据行数对应的储存数据位置的数值是否为1判断是否绘制里面的点 + * 列表显示数值,与多选框原理相似,但不涉及修改操作 + * 列表展开动画,初始化列表时,可以选择列表从头开始展开,或者从上次选中的位置展开 + * 图标展开动画,初始化磁贴类界面时,可以选择图标从头开始展开,或者从上次选中的位置展开 + * 弹出窗口,实现了窗口弹出的动画效果,可以自定义最大值,最小值,步进值,需要修改的参数等,窗口独立运行,调用非常简单 + * 弹出窗口背景虚化可选项,背景虚化会产生卡顿感,但删掉代码有些可惜,因此做成可选项,默认关闭 + * 亮度调节,在弹出窗口中调节亮度值可以实时改变当前亮度值 + * 旋钮功能,使用EC11旋钮控制,旋钮方向可以软件调整,内置一个USB控制电脑的示例,在睡眠模式下旋转调整音量或者亮度,短按输入一个键值,长按进入主菜单,旋钮消抖时长等参数可以在弹出窗口中调整 + * 循环模式,选择项超过头尾时,选择项跳转到另一侧继续,列表和磁贴类可以分别选择 + * 黑暗模式,其实本来就是黑暗模式,是增加了白天模式,默认开启黑暗模式 + * 消失动画适配两种模式,一种是渐变成全黑,另一种渐变成全白 + * 断电存储,用简单直观的方式将每种功能参数写入EEPROM,只在修改过参数,进入睡眠模式时写入,避免重复擦写,初始化时检查11个标志位,允许一位误码 + + 项目参考: + + * B站:路徍要什么自行车:在线仿真:https://wokwi.com/projects/350306511434547796,https://www.bilibili.com/video/BV1HA411S7pv/ + * Github:createskyblue:OpenT12:https://github.com/createskyblue/OpenT12 + * Github:peng-zhihui:OpenHeat:https://github.com/peng-zhihui/OpenHeat + + 注意事项: + + * 示例使用的单片机为 STM32F103C8T6 ,旋钮为 EC11 ,旋钮旋转使用中断程序,旋钮质量不好可能使界面卡死,原因不明,如果能查明原因请联系我 + * 为防止使用者在上传程序后无法直接使用,认为是代码有问题,HID功能默认禁用,如需使用旋钮音量控制和点按输入功能,请在初始化函数中启用相关功能,上传成功后设置两个跳线帽都为0,断电再插上 USB 线 + + 本项目使用Apache 2.0开源协议,如需商用或借鉴,请在醒目处标注本项目开源地址 + 欢迎关注我的B站账号,用户名:音游玩的人,B站主页:https://space.bilibili.com/9182439?spm_id_from=..0.0 +*/ + +/************************************* 屏幕驱动 *************************************/ + +//分辨率128*32,可以使用硬件IIC接口 + +#include +#include + +#define SCL PB6 +#define SDA PB7 +#define RST U8X8_PIN_NONE + +U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C u8g2(U8G2_R0, SCL, SDA, RST); // 分辨率:128*32 驱动:SSD1306 接口:IIC(硬件) + +/************************************* 定义页面 *************************************/ + +//总目录,缩进表示页面层级 +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 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[] +{ + {"[ WouoUI ]"}, + {"- Version: v2.0"}, + {"- Board: STM32F103"}, + {"- Ram: 20k"}, + {"- Flash: 64k"}, + {"- Freq: 72Mhz"}, + {"- Creator: RQNG"}, + {"- Billi UID: 9182439"}, +}; + +/************************************* 图片内容 *************************************/ + +PROGMEM const uint8_t main_icon_pic[][16*7] +{ + { + 0xFF,0xFF,0xFF,0x0F,0xFF,0xFF,0xFF,0x0F,0xFF,0xFF,0xF8,0x0F,0xFF,0xFF,0xE1,0x0F, + 0xFF,0xFF,0xC3,0x0F,0xFF,0xFF,0x83,0x0F,0xFF,0xFF,0x07,0x0F,0xFF,0xFF,0x07,0x0F, + 0xFF,0xFF,0x07,0x0E,0xFF,0xFF,0x07,0x0E,0xFF,0xFF,0x07,0x0C,0xFF,0xFF,0x07,0x0C, + 0xFF,0xFF,0x07,0x0C,0xFF,0xFF,0x03,0x0C,0xFF,0xFF,0x03,0x0C,0xFF,0xFF,0x01,0x0C, + 0xFB,0xFF,0x00,0x0C,0xF3,0x7F,0x00,0x0E,0xC3,0x1F,0x00,0x0E,0x07,0x00,0x00,0x0F, + 0x07,0x00,0x00,0x0F,0x0F,0x00,0x80,0x0F,0x1F,0x00,0xC0,0x0F,0x3F,0x00,0xE0,0x0F, + 0xFF,0x00,0xF8,0x0F,0xFF,0x03,0xFE,0x0F,0xFF,0xFF,0xFF,0x0F,0xFF,0xFF,0xFF,0x0F, + }, + { + 0xFF,0xFF,0xFF,0x0F,0xFF,0xFF,0xFF,0x0F,0xFF,0xFC,0xF3,0x0F,0xFF,0xFC,0xF3,0x0F, + 0xFF,0xFC,0xF3,0x0F,0x7F,0xF8,0xF3,0x0F,0x3F,0xF0,0xF3,0x0F,0x3F,0xF0,0xE1,0x0F, + 0x3F,0xF0,0xE1,0x0F,0x3F,0xF0,0xF3,0x0F,0x7F,0xF8,0xF3,0x0F,0xFF,0xFC,0xF3,0x0F, + 0xFF,0xFC,0xF3,0x0F,0xFF,0xFC,0xF3,0x0F,0xFF,0xFC,0xF3,0x0F,0xFF,0xFC,0xF3,0x0F, + 0xFF,0xFC,0xF3,0x0F,0xFF,0xFC,0xE1,0x0F,0xFF,0xFC,0xC0,0x0F,0x7F,0xF8,0xC0,0x0F, + 0x7F,0xF8,0xC0,0x0F,0xFF,0xFC,0xC0,0x0F,0xFF,0xFC,0xE1,0x0F,0xFF,0xFC,0xF3,0x0F, + 0xFF,0xFC,0xF3,0x0F,0xFF,0xFC,0xF3,0x0F,0xFF,0xFF,0xFF,0x0F,0xFF,0xFF,0xFF,0x0F, + }, + { + 0xFF,0xFF,0xFF,0x0F,0xF7,0xFF,0xFF,0x0F,0xE3,0xFF,0xFF,0x0F,0xE3,0xFF,0xFF,0x0F, + 0xC1,0xFF,0xFF,0x0F,0xF7,0xF8,0xFF,0x0F,0x37,0xE0,0xFF,0x0F,0x37,0xE7,0xFF,0x0F, + 0x97,0xCF,0xFF,0x0F,0x97,0xCF,0xFF,0x0F,0xC7,0x9F,0xFF,0x0F,0xC7,0x9F,0xFF,0x0F, + 0xE7,0x3F,0xFF,0x0F,0xE7,0x3F,0xFF,0x0C,0xF7,0x7F,0xFE,0x0C,0xF7,0x7F,0xFE,0x0C, + 0xF7,0xFF,0x7C,0x0E,0xF7,0xFF,0x7C,0x0E,0xF7,0xFF,0x39,0x0F,0xF7,0xFF,0x01,0x0F, + 0xF7,0xFF,0xC7,0x0F,0xF7,0xFF,0xFF,0x0F,0xF7,0xFF,0x7F,0x0F,0xF7,0xFF,0x7F,0x0C, + 0x07,0x00,0x00,0x08,0xFF,0xFF,0x7F,0x0C,0xFF,0xFF,0x7F,0x0F,0xFF,0xFF,0xFF,0x0F, + }, + { + 0xFF,0xFF,0xFF,0x0F,0xFF,0xFF,0xFF,0x0F,0xFF,0x9F,0xFF,0x0F,0xFF,0x0F,0xFF,0x0F, + 0xFF,0x0F,0xFF,0x0F,0x3F,0x0F,0xCF,0x0F,0x1F,0x02,0x84,0x0F,0x1F,0x00,0x80,0x0F, + 0x3F,0x00,0xC0,0x0F,0x7F,0x60,0xE0,0x0F,0x3F,0xF8,0xC1,0x0F,0x3F,0xFC,0xC3,0x0F, + 0x07,0xFC,0x03,0x0E,0x03,0xFE,0x07,0x0C,0x03,0xFE,0x07,0x0C,0x07,0xFC,0x03,0x0E, + 0x3F,0xFC,0xC3,0x0F,0x3F,0xF8,0xC1,0x0F,0x7F,0x60,0xE0,0x0F,0x3F,0x00,0xC0,0x0F, + 0x1F,0x00,0x80,0x0F,0x1F,0x02,0x84,0x0F,0x3F,0x0F,0xCF,0x0F,0xFF,0x0F,0xFF,0x0F, + 0xFF,0x0F,0xFF,0x0F,0xFF,0x9F,0xFF,0x0F,0xFF,0xFF,0xFF,0x0F,0xFF,0xFF,0xFF,0x0F, + }, +}; + +/************************************* 页面变量 *************************************/ + +//OLED变量 +#define DISP_H 32 //屏幕高度 +#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_SLEEP; + uint8_t state = S_NONE; + bool sleep = true; + uint8_t fade = 1; + uint8_t param[UI_PARAM]; +} ui; + +//磁贴变量 +//没有定义文字列表的磁贴类,需要定义图标数量 +#define MAIN_ICON_NUMB 4 +//所有磁贴页面都使用同一套参数 +#define TILE_ICON_H 28 //磁贴图标高度 +#define TILE_ICON_W 28 //磁贴图标宽度 +#define TILE_ICON_S 32 //磁贴图标间距 +#define TILE_INDI_H 2 //磁贴大标题指示器高度 +#define TILE_INDI_W 14 //磁贴大标题指示器宽度 +struct +{ + 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; + +//电压测量页面变量 +//开发板模拟引脚 +uint8_t analog_pin[10] = { PA0, PA1, PA2, PA3, PA4, PA5, PA6, PA7, PB0, PB1 }; +//曲线相关 +#define WAVE_SAMPLE 20 //采集倍数 +#define WAVE_W 32 //波形宽度 +#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 32 //波形边框宽度 +#define WAVE_BOX_L_S 24 //波形边框左边距 +//列表和文字背景框相关 +#define VOLT_FONT u8g2_font_helvB18_tr //电压数字字体 +#define VOLT_TEXT_BG_L_S 59 //文字背景框左边距 +#define VOLT_TEXT_BG_W 59 //文字背景框宽度 +#define VOLT_TEXT_BG_H 32 //文字背景框高度 +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; + +/************************************* USB 相关 *************************************/ + +#include + +USBHID HID; + +const uint8_t reportDescription[] = +{ + HID_CONSUMER_REPORT_DESCRIPTOR(), + HID_KEYBOARD_REPORT_DESCRIPTOR() +}; + +HIDConsumer Consumer(HID); +HIDKeyboard Keyboard(HID); + +void hid_init() +{ + HID.begin(reportDescription, sizeof(reportDescription)); + while (!USBComposite); +} + +/********************************** 自定义功能变量 **********************************/ + +//旋钮功能变量 +#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变量 +#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() +{ + 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; +} + +//EEPROM读数据,开机初始化时执行一遍 +void eeprom_read_all_data() +{ + 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; +} + +//开机检查是否已经修改过,没修改过则跳过读配置步骤,用默认设置 +void eeprom_init() +{ + 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(); +} + +/************************************* 旋钮相关 *************************************/ + +//可按下旋钮引脚 +#define AIO PB12 +#define BIO PB13 +#define SW PB14 + +//按键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; +} btn; + +void knob_inter() +{ + 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; + } +} + +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; + } + } +} + +void btn_init() +{ + pinMode(AIO, INPUT); + pinMode(BIO, INPUT); + pinMode(SW, INPUT_PULLUP); + attachInterrupt(digitalPinToInterrupt(AIO), knob_inter, CHANGE); +} + +/************************************ 初始化函数 ***********************************/ + +/********************************* 初始化数据处理函数 *******************************/ + +//显示数值的初始化 +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] = MAIN_ICON_NUMB; //没有文字的磁贴类要在这里添加该页面图标数量 + 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; +} + +//进入睡眠时的初始化 +void sleep_param_init() +{ + u8g2.setDrawColor(0); + u8g2.drawBox(0, 0, DISP_W, DISP_H); + u8g2.setPowerSave(1); + 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(const uint8_t icon_pic[][16*7]) +{ + //计算动画过渡值 + 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); + + //设置大标题的颜色,0透显,1实显,2反色,这里用实显 + u8g2.setDrawColor(1); + + //绘制图标指示器 + u8g2.drawBox((DISP_W - tile.indi_x) / 2, DISP_H - TILE_INDI_H, 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]); + } + 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.setDrawColor(2); + if (!ui.param[DARK_MODE]) u8g2.drawBox(0, 0, DISP_W, DISP_H); +} + +/*************** 根据列表每行开头符号,判断每行尾部是否绘制以及绘制什么内容 *************/ + +//列表显示数值 +void list_draw_value(int n) { 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); } + +//绘制框里面的点 +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); } + +//列表显示旋钮功能 +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); + 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); + list.box_x_trg = u8g2.getStrWidth(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.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); + + //绘制列表文字 + 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); + + //反转屏幕内元素颜色,白天模式遮罩,在这里屏蔽有列表参与的页面,使遮罩作用在那个页面上 + if (!ui.param[DARK_MODE]) + { + u8g2.drawBox(0, 0, DISP_W, DISP_H); + switch(ui.index) + { + case M_WINDOW: + case M_VOLT: + u8g2.drawBox(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); + + //绘制电压曲线和外框 + 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); + 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 = analogRead(analog_pin[ui.select[ui.layer]]); + for (int i = 1; i < WAVE_W - 1; i++) + { + volt.ch0_wave[i] = 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]); + } + } + + //绘制电压值 + u8g2.setFontDirection(0); + u8g2.setFont(VOLT_FONT); + u8g2.setCursor(66, DISP_H - 7); + u8g2.print(volt.val / 4096.0 * 3.3); + + //绘制列表选择框和电压文字背景 + 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); +} + +//弹窗通用显示函数 +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); + 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); + + //绘制窗口 + 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); +} + +/************************************* 处理函数 *************************************/ + +/*********************************** 通用处理函数 ***********************************/ + +//磁贴类页面旋转时判断通用函数 +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_CW: if (*win.value < win.max) *win.value += win.step; eeprom.change = true; break; + case BTN_ID_CC: 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(); + + //当旋钮有动作时 + if (btn.pressed) { btn.pressed = false; switch (btn.id) { + + //睡眠时顺时针旋转执行的函数 + 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; + + //睡眠时长按执行的函数 + case BTN_ID_LP: ui.index = M_MAIN; ui.state = S_LAYER_IN; u8g2.setPowerSave(0); ui.sleep = false; break; + } + } + } +} + +//主菜单处理函数,磁贴类模板 +void main_proc() +{ + tile_show(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; + } +} + +//编辑器菜单处理函数 +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; + } + } + } +} + +//旋钮点按功能菜单处理函数 +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(); + 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(); 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; + } + } +} + +//OLED初始化函数 +void oled_init() +{ + u8g2.setBusClock(1000000); //硬件IIC接口使用 + u8g2.begin(); + u8g2.setContrast(ui.param[DISP_BRI]); + buf_ptr = u8g2.getBufferPtr(); + buf_len = 8 * u8g2.getBufferTileHeight() * u8g2.getBufferTileWidth(); +} + +void setup() +{ + eeprom_init(); + ui_init(); + oled_init(); + btn_init(); + //hid_init(); +} + +void loop() +{ + btn_scan(); + ui_proc(); +} + + diff --git a/WouoUI-128_64/WouoUI-128_64.ino b/WouoUI-128_64/WouoUI-128_64.ino new file mode 100644 index 0000000..2f74219 --- /dev/null +++ b/WouoUI-128_64/WouoUI-128_64.ino @@ -0,0 +1,1619 @@ + +/* + 此项目模仿自稚晖君暂未开源的 MonoUI,用于实现类似 UltraLink 的丝滑界面 + + 有四个版本: + + * 分辨率 128 * 128 :主菜单,列表和关于本机页基本还原了 UltraLink 的界面和动态效果,电压测量页为原创设计 + * 分辨率 128 * 64 :主菜单模仿 UltraLink 重新设计,去掉了小标题,电压测量页重新设计,关于本机页面改为列表,列表适配了该分辨率 + * 分辨率 128 * 32 :在 128 * 64 分辨率的基础上,主界面只保留图标,电压测量页重新设计 + * 通用版本:仅保留列表,主菜单也改为列表,删除电压测量页和与列表无关的动画(保留弹窗效果),经过简单修改可以适配任何分辨率,任何行高度的情况 + + WouoUI v2.0 功能: + + * 全部使用非线性的平滑缓动动画,包括列表,弹窗,甚至进度条 + * 优化平滑动画算法到只有两行,分类别定义平滑权重,并且每个权重值都分别可调 + * 可以打断的非线性动画,当前动画未结束但下一次动画已经被触发时,动画可以自然过渡 + * 非通用版本分别适配了类似 UltraLink 主菜单的磁贴界面(因为让我想起 WP7 的 Metron 风格,所以称之为磁贴) + * 通用版本仅保留列表类界面,经过简单修改可以适配所有分辨率的屏幕,包括屏幕内行数不是整数的情况 + * 列表菜单,列表可以无限延长 + * 列表文字选择框,选择框可根据选择的字符串长度自动伸缩,进入菜单时从列表开头从长度 0 展开,转到上一级列表时,长度和纵坐标平滑移动到上一级选择的位置 + * 列表单选框,储存数据时也储存该值在列表中所在的位置,展开列表时根据每行开头的字符判断是否绘制外框,再根据位置数据判断是否绘制里面的点 + * 列表多选框,储存数据的数组跟多选框列表的行数对应,不要求连续排列,展开列表时根据每行开头的字符判断是否绘制外框,再根据行数对应的储存数据位置的数值是否为1判断是否绘制里面的点 + * 列表显示数值,与多选框原理相似,但不涉及修改操作 + * 列表展开动画,初始化列表时,可以选择列表从头开始展开,或者从上次选中的位置展开 + * 图标展开动画,初始化磁贴类界面时,可以选择图标从头开始展开,或者从上次选中的位置展开 + * 弹出窗口,实现了窗口弹出的动画效果,可以自定义最大值,最小值,步进值,需要修改的参数等,窗口独立运行,调用非常简单 + * 弹出窗口背景虚化可选项,背景虚化会产生卡顿感,但删掉代码有些可惜,因此做成可选项,默认关闭 + * 亮度调节,在弹出窗口中调节亮度值可以实时改变当前亮度值 + * 旋钮功能,使用EC11旋钮控制,旋钮方向可以软件调整,内置一个USB控制电脑的示例,在睡眠模式下旋转调整音量或者亮度,短按输入一个键值,长按进入主菜单,旋钮消抖时长等参数可以在弹出窗口中调整 + * 循环模式,选择项超过头尾时,选择项跳转到另一侧继续,列表和磁贴类可以分别选择 + * 黑暗模式,其实本来就是黑暗模式,是增加了白天模式,默认开启黑暗模式 + * 消失动画适配两种模式,一种是渐变成全黑,另一种渐变成全白 + * 断电存储,用简单直观的方式将每种功能参数写入EEPROM,只在修改过参数,进入睡眠模式时写入,避免重复擦写,初始化时检查11个标志位,允许一位误码 + + 项目参考: + + * B站:路徍要什么自行车:在线仿真:https://wokwi.com/projects/350306511434547796,https://www.bilibili.com/video/BV1HA411S7pv/ + * Github:createskyblue:OpenT12:https://github.com/createskyblue/OpenT12 + * Github:peng-zhihui:OpenHeat:https://github.com/peng-zhihui/OpenHeat + + 注意事项: + + * 示例使用的单片机为 STM32F103C8T6 ,旋钮为 EC11 ,旋钮旋转使用中断程序,旋钮质量不好可能使界面卡死,原因不明,如果能查明原因请联系我 + * 为防止使用者在上传程序后无法直接使用,认为是代码有问题,HID功能默认禁用,如需使用旋钮音量控制和点按输入功能,请在初始化函数中启用相关功能,上传成功后设置两个跳线帽都为0,断电再插上 USB 线 + + 本项目使用Apache 2.0开源协议,如需商用或借鉴,请在醒目处标注本项目开源地址 + 欢迎关注我的B站账号,用户名:音游玩的人,B站主页:https://space.bilibili.com/9182439?spm_id_from=..0.0 +*/ + +/************************************* 屏幕驱动 *************************************/ + +//分辨率128*64,可以使用硬件IIC接口 + +#include +#include + +#define SCL PB6 +#define SDA PB7 +#define RST U8X8_PIN_NONE + +U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, SCL, SDA, RST); // 分辨率:128*64 驱动:SSD1306 接口:IIC(硬件) + +/************************************* 定义页面 *************************************/ + +//总目录,缩进表示页面层级 +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[] +{ + {"[ WouoUI ]"}, + {"- Version: v2.0"}, + {"- Board: STM32F103"}, + {"- Ram: 20k"}, + {"- Flash: 64k"}, + {"- Freq: 72Mhz"}, + {"- Creator: RQNG"}, + {"- Billi UID: 9182439"}, +}; + +/************************************* 图片内容 *************************************/ + +PROGMEM 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_SLEEP; + uint8_t state = S_NONE; + 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; + +//电压测量页面变量 +//开发板模拟引脚 +uint8_t analog_pin[10] = { PA0, PA1, PA2, PA3, PA4, PA5, PA6, PA7, PB0, PB1 }; +//曲线相关 +#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; + +/************************************* USB 相关 *************************************/ + +#include + +USBHID HID; + +const uint8_t reportDescription[] = +{ + HID_CONSUMER_REPORT_DESCRIPTOR(), + HID_KEYBOARD_REPORT_DESCRIPTOR() +}; + +HIDConsumer Consumer(HID); +HIDKeyboard Keyboard(HID); + +void hid_init() +{ + HID.begin(reportDescription, sizeof(reportDescription)); + while (!USBComposite); +} + +/********************************** 自定义功能变量 **********************************/ + +//旋钮功能变量 +#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变量 +#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() +{ + 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; +} + +//EEPROM读数据,开机初始化时执行一遍 +void eeprom_read_all_data() +{ + 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; +} + +//开机检查是否已经修改过,没修改过则跳过读配置步骤,用默认设置 +void eeprom_init() +{ + 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(); +} + +/************************************* 旋钮相关 *************************************/ + +//可按下旋钮引脚 +#define AIO PB12 +#define BIO PB13 +#define SW PB14 + +//按键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; +} btn; + +void knob_inter() +{ + 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; + } +} + +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; + } + } +} + +void btn_init() +{ + pinMode(AIO, INPUT); + pinMode(BIO, INPUT); + pinMode(SW, INPUT_PULLUP); + attachInterrupt(digitalPinToInterrupt(AIO), knob_inter, CHANGE); +} + +/************************************ 初始化函数 ***********************************/ + +/********************************* 初始化数据处理函数 *******************************/ + +//显示数值的初始化 +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() +{ + u8g2.setDrawColor(0); + u8g2.drawBox(0, 0, DISP_W, DISP_H); + u8g2.setPowerSave(1); + 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.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.drawBox(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]); + } + 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.setDrawColor(2); + if (!ui.param[DARK_MODE]) u8g2.drawBox(0, 0, DISP_W, DISP_H); +} + +/*************** 根据列表每行开头符号,判断每行尾部是否绘制以及绘制什么内容 *************/ + +//列表显示数值 +void list_draw_value(int n) { 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); } + +//绘制框里面的点 +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); } + +//列表显示旋钮功能 +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); + 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); + list.box_x_trg = u8g2.getStrWidth(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.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); + + //绘制列表文字 + 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); + + //反转屏幕内元素颜色,白天模式遮罩,在这里屏蔽有列表参与的页面,使遮罩作用在那个页面上 + if (!ui.param[DARK_MODE]) + { + u8g2.drawBox(0, 0, DISP_W, DISP_H); + switch(ui.index) + { + case M_WINDOW: + case M_VOLT: + u8g2.drawBox(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); + + //绘制电压曲线和外框 + 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); + 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 = analogRead(analog_pin[ui.select[ui.layer]]); + for (int i = 1; i < WAVE_W - 1; i++) + { + volt.ch0_wave[i] = 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]); + } + } + + //绘制电压值 + 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); +} + +//弹窗通用显示函数 +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); + 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); + + //绘制窗口 + 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); +} + +/************************************* 处理函数 *************************************/ + +/*********************************** 通用处理函数 ***********************************/ + +//磁贴类页面旋转时判断通用函数 +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_CW: if (*win.value < win.max) *win.value += win.step; eeprom.change = true; break; + case BTN_ID_CC: 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(); + + //当旋钮有动作时 + if (btn.pressed) { btn.pressed = false; switch (btn.id) { + + //睡眠时顺时针旋转执行的函数 + 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; + + //睡眠时长按执行的函数 + case BTN_ID_LP: ui.index = M_MAIN; ui.state = S_LAYER_IN; u8g2.setPowerSave(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; + } + } + } +} + +//旋钮点按功能菜单处理函数 +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(); + 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(); 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; + } + } +} + +//OLED初始化函数 +void oled_init() +{ + u8g2.setBusClock(1000000); //硬件IIC接口使用 + u8g2.begin(); + u8g2.setContrast(ui.param[DISP_BRI]); + buf_ptr = u8g2.getBufferPtr(); + buf_len = 8 * u8g2.getBufferTileHeight() * u8g2.getBufferTileWidth(); +} + +void setup() +{ + eeprom_init(); + ui_init(); + oled_init(); + btn_init(); + //hid_init(); +} + +void loop() +{ + btn_scan(); + ui_proc(); +} + + diff --git a/WouoUI-Lite_General/WouoUI-Lite_General.ino b/WouoUI-Lite_General/WouoUI-Lite_General.ino new file mode 100644 index 0000000..ba2abc5 --- /dev/null +++ b/WouoUI-Lite_General/WouoUI-Lite_General.ino @@ -0,0 +1,1295 @@ + +/* + 此项目模仿自稚晖君暂未开源的 MonoUI,用于实现类似 UltraLink 的丝滑界面 + + 有四个版本: + + * 分辨率 128 * 128 :主菜单,列表和关于本机页基本还原了 UltraLink 的界面和动态效果,电压测量页为原创设计 + * 分辨率 128 * 64 :主菜单模仿 UltraLink 重新设计,去掉了小标题,电压测量页重新设计,关于本机页面改为列表,列表适配了该分辨率 + * 分辨率 128 * 32 :在 128 * 64 分辨率的基础上,主界面只保留图标,电压测量页重新设计 + * 通用版本:仅保留列表,主菜单也改为列表,删除电压测量页和与列表无关的动画(保留弹窗效果),经过简单修改可以适配任何分辨率,任何行高度的情况 + + WouoUI v2.0 功能: + + * 全部使用非线性的平滑缓动动画,包括列表,弹窗,甚至进度条 + * 优化平滑动画算法到只有两行,分类别定义平滑权重,并且每个权重值都分别可调 + * 可以打断的非线性动画,当前动画未结束但下一次动画已经被触发时,动画可以自然过渡 + * 非通用版本分别适配了类似 UltraLink 主菜单的磁贴界面(因为让我想起 WP7 的 Metron 风格,所以称之为磁贴) + * 通用版本仅保留列表类界面,经过简单修改可以适配所有分辨率的屏幕,包括屏幕内行数不是整数的情况 + * 列表菜单,列表可以无限延长 + * 列表文字选择框,选择框可根据选择的字符串长度自动伸缩,进入菜单时从列表开头从长度 0 展开,转到上一级列表时,长度和纵坐标平滑移动到上一级选择的位置 + * 列表单选框,储存数据时也储存该值在列表中所在的位置,展开列表时根据每行开头的字符判断是否绘制外框,再根据位置数据判断是否绘制里面的点 + * 列表多选框,储存数据的数组跟多选框列表的行数对应,不要求连续排列,展开列表时根据每行开头的字符判断是否绘制外框,再根据行数对应的储存数据位置的数值是否为1判断是否绘制里面的点 + * 列表显示数值,与多选框原理相似,但不涉及修改操作 + * 列表展开动画,初始化列表时,可以选择列表从头开始展开,或者从上次选中的位置展开 + * 图标展开动画,初始化磁贴类界面时,可以选择图标从头开始展开,或者从上次选中的位置展开 + * 弹出窗口,实现了窗口弹出的动画效果,可以自定义最大值,最小值,步进值,需要修改的参数等,窗口独立运行,调用非常简单 + * 弹出窗口背景虚化可选项,背景虚化会产生卡顿感,但删掉代码有些可惜,因此做成可选项,默认关闭 + * 亮度调节,在弹出窗口中调节亮度值可以实时改变当前亮度值 + * 旋钮功能,使用EC11旋钮控制,旋钮方向可以软件调整,内置一个USB控制电脑的示例,在睡眠模式下旋转调整音量或者亮度,短按输入一个键值,长按进入主菜单,旋钮消抖时长等参数可以在弹出窗口中调整 + * 循环模式,选择项超过头尾时,选择项跳转到另一侧继续,列表和磁贴类可以分别选择 + * 黑暗模式,其实本来就是黑暗模式,是增加了白天模式,默认开启黑暗模式 + * 消失动画适配两种模式,一种是渐变成全黑,另一种渐变成全白 + * 断电存储,用简单直观的方式将每种功能参数写入EEPROM,只在修改过参数,进入睡眠模式时写入,避免重复擦写,初始化时检查11个标志位,允许一位误码 + + 项目参考: + + * B站:路徍要什么自行车:在线仿真:https://wokwi.com/projects/350306511434547796,https://www.bilibili.com/video/BV1HA411S7pv/ + * Github:createskyblue:OpenT12:https://github.com/createskyblue/OpenT12 + * Github:peng-zhihui:OpenHeat:https://github.com/peng-zhihui/OpenHeat + + 注意事项: + + * 示例使用的单片机为 STM32F103C8T6 ,旋钮为 EC11 ,旋钮旋转使用中断程序,旋钮质量不好可能使界面卡死,原因不明,如果能查明原因请联系我 + * 为防止使用者在上传程序后无法直接使用,认为是代码有问题,HID功能默认禁用,如需使用旋钮音量控制和点按输入功能,请在初始化函数中启用相关功能,上传成功后设置两个跳线帽都为0,断电再插上 USB 线 + + 本项目使用Apache 2.0开源协议,如需商用或借鉴,请在醒目处标注本项目开源地址 + 欢迎关注我的B站账号,用户名:音游玩的人,B站主页:https://space.bilibili.com/9182439?spm_id_from=..0.0 +*/ + +/************************************* 屏幕驱动 *************************************/ + +//分辨率128*64,可以使用硬件IIC接口 + +#include +#include + +#define SCL PB6 +#define SDA PB7 +#define RST U8X8_PIN_NONE + +U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, SCL, SDA, RST); // 分辨率:128*64 驱动:SSD1306 接口:IIC(硬件) + +/************************************* 定义页面 *************************************/ + +//总目录,缩进表示页面层级 +enum +{ + M_WINDOW, + M_SLEEP, + M_MAIN, + M_EDITOR, + M_KNOB, + M_KRF, + M_KPF, + 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[] +{ + {"[ Main Menu ]"}, + {"- Editor"}, + {"- 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 setting_menu[] +{ + {"[ Setting ]"}, + {"~ Disp Bri"}, + {"~ List Ani"}, + {"~ Win Ani"}, + {"~ Fade Ani"}, + {"~ Btn SPT"}, + {"~ Btn LPT"}, + {"+ L Ufd Fm Scr"}, + {"+ L Loop Mode"}, + {"+ Win Bokeh Bg"}, + {"+ Knob Rot Dir"}, + {"+ Dark Mode"}, + {"- [ About ]"}, +}; + +M_SELECT about_menu[] +{ + {"[ WouoUI ]"}, + {"- Version: v2.0"}, + {"- Board: STM32F103"}, + {"- Ram: 20k"}, + {"- Flash: 64k"}, + {"- Freq: 72Mhz"}, + {"- Creator: RQNG"}, + {"- Billi UID: 9182439"}, +}; + +/************************************* 页面变量 *************************************/ + +//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 11 //参数数量 +enum +{ + DISP_BRI, //屏幕亮度 + LIST_ANI, //列表动画速度 + WIN_ANI, //弹窗动画速度 + FADE_ANI, //消失动画速度 + BTN_SPT, //按键短按时长 + BTN_LPT, //按键长按时长 + LIST_UFD, //菜单列表从头展开开关 + 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_SLEEP; + uint8_t state = S_NONE; + bool sleep = true; + uint8_t fade = 1; + uint8_t param[UI_PARAM]; +} ui; + +//列表变量 +//默认参数 +/* +#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 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; + +/************************************* USB 相关 *************************************/ + +#include + +USBHID HID; + +const uint8_t reportDescription[] = +{ + HID_CONSUMER_REPORT_DESCRIPTOR(), + HID_KEYBOARD_REPORT_DESCRIPTOR() +}; + +HIDConsumer Consumer(HID); +HIDKeyboard Keyboard(HID); + +void hid_init() +{ + HID.begin(reportDescription, sizeof(reportDescription)); + while (!USBComposite); +} + +/********************************** 自定义功能变量 **********************************/ + +//旋钮功能变量 +#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变量 +#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() +{ + 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; +} + +//EEPROM读数据,开机初始化时执行一遍 +void eeprom_read_all_data() +{ + 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; +} + +//开机检查是否已经修改过,没修改过则跳过读配置步骤,用默认设置 +void eeprom_init() +{ + 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(); +} + +/************************************* 旋钮相关 *************************************/ + +//可按下旋钮引脚 +#define AIO PB12 +#define BIO PB13 +#define SW PB14 + +//按键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; +} btn; + +void knob_inter() +{ + 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; + } +} + +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; + } + } +} + +void btn_init() +{ + pinMode(AIO, INPUT); + pinMode(BIO, INPUT); + pinMode(SW, INPUT_PULLUP); + attachInterrupt(digitalPinToInterrupt(AIO), knob_inter, CHANGE); +} + +/************************************ 初始化函数 ***********************************/ + +/********************************* 初始化数据处理函数 *******************************/ + +//显示数值的初始化 +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[LIST_ANI] = 60; //列表动画速度 + ui.param[WIN_ANI] = 25; //弹窗动画速度 + ui.param[FADE_ANI] = 30; //消失动画速度 + ui.param[BTN_SPT] = 25; //按键短按时长 + ui.param[BTN_LPT] = 150; //按键长按时长 + ui.param[LIST_UFD] = 1; //菜单列表从头展开开关 + 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_SETTING] = sizeof( setting_menu ) / sizeof(M_SELECT); + ui.num[M_ABOUT] = sizeof( about_menu ) / sizeof(M_SELECT); +} + +/********************************* 分页面初始化函数 ********************************/ + +//进入睡眠时的初始化 +void sleep_param_init() +{ + u8g2.setDrawColor(0); + u8g2.drawBox(0, 0, DISP_W, DISP_H); + u8g2.setPowerSave(1); + 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 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_KNOB: knob_param_init(); break; //旋钮设置页,行末尾文字初始化 + case M_KRF: krf_param_init(); break; //旋钮旋转页,单选框初始化 + case M_KPF: kpf_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 + } +} + +/************************************* 动画函数 *************************************/ + +//动画函数 +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 list_draw_value(int n) { 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); } + +//绘制框里面的点 +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); } + +//列表显示旋钮功能 +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); + 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); + list.box_x_trg = u8g2.getStrWidth(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.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); + + //绘制列表文字 + 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); + + //反转屏幕内元素颜色,白天模式遮罩,在这里屏蔽有列表参与的页面,使遮罩作用在那个页面上 + if (!ui.param[DARK_MODE]) + { + u8g2.drawBox(0, 0, DISP_W, DISP_H); + switch(ui.index) + { + case M_WINDOW: + u8g2.drawBox(0, 0, DISP_W, DISP_H); + } + } +} + +//弹窗通用显示函数 +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); + 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); + + //绘制窗口 + 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); +} + +/************************************* 处理函数 *************************************/ + +/*********************************** 通用处理函数 ***********************************/ + +//列表类页面旋转时判断通用函数 +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_CW: if (*win.value < win.max) *win.value += win.step; eeprom.change = true; break; + case BTN_ID_CC: 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(); + + //当旋钮有动作时 + if (btn.pressed) { btn.pressed = false; switch (btn.id) { + + //睡眠时顺时针旋转执行的函数 + 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; + + //睡眠时长按执行的函数 + case BTN_ID_LP: ui.index = M_MAIN; ui.state = S_LAYER_IN; u8g2.setPowerSave(0); ui.sleep = false; break; + } + } + } +} + +//主菜单处理函数,无选项框列表类模板 +void main_proc() +{ + list_show(main_menu, M_MAIN); + 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_SLEEP; ui.state = S_LAYER_OUT; break; + case 1: ui.index = M_EDITOR; ui.state = S_LAYER_IN; break; + case 2: ui.index = M_SETTING; ui.state = S_LAYER_IN; break; + } + } + } +} + +//编辑器菜单处理函数 +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; + } + } + } +} + +//旋钮点按功能菜单处理函数 +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 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("List Ani", LIST_ANI, &ui.param[LIST_ANI], 100, 10, 1, setting_menu, M_SETTING); break; + case 3: window_value_init("Win Ani", WIN_ANI, &ui.param[WIN_ANI], 100, 10, 1, setting_menu, M_SETTING); break; + case 4: window_value_init("Fade Ani", FADE_ANI, &ui.param[FADE_ANI], 255, 0, 1, setting_menu, M_SETTING); break; + case 5: window_value_init("Btn SPT", BTN_SPT, &ui.param[BTN_SPT], 255, 0, 1, setting_menu, M_SETTING); break; + case 6: window_value_init("Btn LPT", BTN_LPT, &ui.param[BTN_LPT], 255, 0, 1, setting_menu, M_SETTING); break; + + //多选框 + case 7: check_box_m_select( LIST_UFD ); break; + case 8: check_box_m_select( LIST_LOOP ); break; + case 9: check_box_m_select( WIN_BOK ); break; + case 10: check_box_m_select( KNOB_DIR ); break; + case 11: check_box_m_select( DARK_MODE ); break; + + //关于本机 + case 12: 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(); + 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(); 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_SETTING: setting_proc(); break; + case M_ABOUT: about_proc(); break; + } + } +} + +//OLED初始化函数 +void oled_init() +{ + u8g2.setBusClock(1000000); //硬件IIC接口使用 + u8g2.begin(); + u8g2.setContrast(ui.param[DISP_BRI]); + buf_ptr = u8g2.getBufferPtr(); + buf_len = 8 * u8g2.getBufferTileHeight() * u8g2.getBufferTileWidth(); +} + +void setup() +{ + eeprom_init(); + ui_init(); + oled_init(); + btn_init(); + //hid_init(); +} + +void loop() +{ + btn_scan(); + ui_proc(); +} + +