smart_chargeESP32/test-menu.ino

409 lines
16 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
SmartCharge - Menu MVP (ESP32 + OLED + Encoder) [U8g2 + кириллица]
- I2C OLED @ 0x3C (сканер уже нашёл)
- USE_SH1106: 1 = SH1106, 0 = SSD1306
- SDA=21, SCL=22
- Encoder: A=32, B=33, BTN=26
*/
#define USE_SH1106 1 // <<< если у тебя SSD1306, поставь 0
#include <Wire.h>
#include <Preferences.h>
#include <Bounce2.h>
#include <U8g2lib.h>
// -------- OLED (U8g2) --------
#if USE_SH1106
// Полный буфер, аппаратный I2C
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /*reset=*/ U8X8_PIN_NONE);
#else
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /*reset=*/ U8X8_PIN_NONE);
#endif
// Шрифт с кириллицей (компактный, читаемый)
#define MENU_FONT u8g2_font_6x12_t_cyrillic
// -------- ENCODER --------
const int ENC_A_PIN = 32;
const int ENC_B_PIN = 33;
const int ENC_BTN_PIN = 26;
Bounce encBtn;
volatile int16_t encDelta = 0;
int8_t transTable[16] = {
0, -1, +1, 2,
+1, 0, 2, -1,
-1, 2, 0, +1,
2, +1, -1, 0
};
inline void encoderPoll() {
uint8_t a = digitalRead(ENC_A_PIN);
uint8_t b = digitalRead(ENC_B_PIN);
static uint8_t last = 0;
uint8_t s = (a << 1) | b;
uint8_t idx = ((last & 0x03) << 2) | (s & 0x03);
int8_t d = transTable[idx & 0x0F];
if (d == +1) encDelta++;
else if (d == -1) encDelta--;
last = s;
}
int16_t encoderReadAndClear() { noInterrupts(); int16_t v=encDelta; encDelta=0; interrupts(); return v; }
uint32_t ms(){ return millis(); }
// -------- PROFILES --------
enum : uint8_t { CHEM_LIION=0, CHEM_LFP=1, CHEM_LTO=2 };
static inline const char* chemToStr(uint8_t c){
switch(c){ case CHEM_LIION: return "Li-ion"; case CHEM_LFP: return "LiFePO4"; case CHEM_LTO: return "LTO"; }
return "Li-ion";
}
struct Profile {
uint8_t chem; // CHEM_*
uint8_t S; // 1..4
uint16_t capacity_mAh; // 100..10000
float rateC; // 0.1..1.0
float ItermA; // A
};
Preferences prefs;
const int NUM_PROFILES = 5;
Profile profiles[NUM_PROFILES];
int lastProfileIndex = 0;
void loadProfiles() {
prefs.begin("sc", true);
for (int i=0;i<NUM_PROFILES;i++){
String k = String("p")+i+"_";
profiles[i].chem = prefs.getUChar((k+"chem").c_str(), i==0?CHEM_LIION:CHEM_LFP);
profiles[i].S = prefs.getUChar((k+"S").c_str(), (i==1)?3:1);
profiles[i].capacity_mAh = prefs.getUShort((k+"cap").c_str(), (i==0)?2500:1000);
profiles[i].rateC = prefs.getFloat((k+"rate").c_str(), 0.5f);
float itdef = (profiles[i].chem==CHEM_LIION?0.05f:0.02f) * profiles[i].capacity_mAh/1000.0f;
profiles[i].ItermA= prefs.getFloat((k+"iterm").c_str(), itdef);
}
lastProfileIndex = prefs.getUChar("lastIdx", 0);
prefs.end();
}
void saveProfile(int i) {
prefs.begin("sc", false);
String k = String("p")+i+"_";
prefs.putUChar((k+"chem").c_str(), profiles[i].chem);
prefs.putUChar((k+"S").c_str(), profiles[i].S);
prefs.putUShort((k+"cap").c_str(), profiles[i].capacity_mAh);
prefs.putFloat((k+"rate").c_str(), profiles[i].rateC);
prefs.putFloat((k+"iterm").c_str(),profiles[i].ItermA);
prefs.end();
}
void saveLastIndex(int idx){ prefs.begin("sc", false); prefs.putUChar("lastIdx", idx); prefs.end(); }
// -------- UI STATE --------
enum UiState {
UI_HOME=0,
UI_START_MODE, UI_START_PROFILE,
UI_START_QUICK_CAP, UI_START_QUICK_RATEC, UI_START_QUICK_ITERM, UI_START_CONFIRM,
UI_PROFILES_LIST, UI_PROFILE_EDIT_FIELD,
UI_EDIT_CHEM, UI_EDIT_S, UI_EDIT_CAP, UI_EDIT_RATEC, UI_EDIT_ITERM,
UI_SETTINGS, UI_SERVICE, UI_MESSAGE
};
UiState ui = UI_HOME;
int cursor=0, topIndex=0;
uint32_t btnDownAt=0, lastClickAt=0;
bool btnWasDown=false, lastClickUsed=false;
enum StartMode { SM_CHARGE=0, SM_DISCHARGE, SM_RINT };
StartMode startMode = SM_CHARGE;
int startProfileIdx = 0;
uint16_t quickCap_mAh = 2500; float quickRateC=0.5f, quickItermA=0.10f;
// -------- DRAW HELPERS (U8g2) --------
void setFont(){ u8g2.setFont(MENU_FONT); }
void drawHeader(const char* t){
setFont();
// белая плашка
u8g2.setDrawColor(1); u8g2.drawBox(0,0,128,12);
// чёрным по белому
u8g2.setDrawColor(0); u8g2.setCursor(2,10); u8g2.print(t);
// вернуть белый цвет рисования
u8g2.setDrawColor(1);
}
void drawFooterHint(const char* s){
setFont();
u8g2.setCursor(0,64-2); // нижняя строка
u8g2.print(s);
}
void drawMenuItem(int y, const String& text, bool sel){
setFont();
if (sel){
u8g2.drawBox(0,y-9,128,11); // выделенная строка
u8g2.setDrawColor(0); u8g2.setCursor(2,y); u8g2.print(text);
u8g2.setDrawColor(1);
} else {
u8g2.setCursor(2,y); u8g2.print(text);
}
}
String fmtFloat(float v,int p){ char b[20]; dtostrf(v,0,p,b); return String(b); }
// -------- INPUT --------
void pollInputs(){
encoderPoll();
encBtn.update();
bool down = !encBtn.read();
if (down && !btnWasDown) btnDownAt = ms();
else if (!down && btnWasDown){
uint32_t t = ms() - btnDownAt;
if (t < 1500){
uint32_t now = ms();
if (now - lastClickAt < 400 && !lastClickUsed) lastClickUsed = true;
else lastClickUsed = false;
lastClickAt = now;
}
}
btnWasDown = down;
}
bool isShortClick(){ static uint32_t lh=0; if(!btnWasDown){uint32_t now=ms(); if(now-lastClickAt<80 && !lastClickUsed && now-lh>150){lh=now; return true;}} return false; }
bool isDoubleClick(){ static uint32_t lh=0; if(!btnWasDown){uint32_t now=ms(); if(now-lastClickAt<120 && lastClickUsed && now-lh>150){lh=now; lastClickUsed=false; return true;}} return false; }
bool isLongPressReleased(){ static uint32_t lh=0; if(!btnWasDown){uint32_t dur=ms()-btnDownAt; if(dur>=1500 && (ms()-lh>200)){lh=ms(); return true;}} return false; }
int readRot(int step=1){ int16_t d=encoderReadAndClear(); if(!d) return 0; return d>0?+step:-step; }
// -------- SCREENS --------
void screenHome(){
u8g2.clearBuffer();
drawHeader("Главное меню");
const char* items[] = {"Старт","Профили","Параметры","Сервис"}; int n=4;
int r=readRot(); if(r){ cursor+=(r>0?1:-1); if(cursor<0) cursor=n-1; if(cursor>=n) cursor=0; }
for(int i=0;i<n;i++) drawMenuItem(12+(i+1)*11, items[i], i==cursor);
drawFooterHint("Нажми: выбор | Держи: назад");
u8g2.sendBuffer();
if(isShortClick()){
if(cursor==0){ui=UI_START_MODE; cursor=0;}
else if(cursor==1){ui=UI_PROFILES_LIST; cursor=0; topIndex=0;}
else if(cursor==2){ui=UI_SETTINGS;}
else {ui=UI_SERVICE;}
}
}
void screenStartMode(){
u8g2.clearBuffer();
drawHeader("Старт: режим");
const char* items[] = {"Заряд","Разряд (1S)","МиллиОм (1S)"}; int n=3;
int r=readRot(); if(r){ cursor+=(r>0?1:-1); if(cursor<0) cursor=n-1; if(cursor>=n) cursor=0; }
for(int i=0;i<n;i++) drawMenuItem(12+(i+1)*11, items[i], i==cursor);
drawFooterHint("Нажми: далее | Держи: назад");
u8g2.sendBuffer();
if(isShortClick()){ startMode=(StartMode)cursor; ui=UI_START_PROFILE; startProfileIdx=lastProfileIndex; }
if(isLongPressReleased()){ ui=UI_HOME; cursor=0; }
}
void screenStartProfile(){
u8g2.clearBuffer();
drawHeader("Старт: профиль");
int n=NUM_PROFILES; int r=readRot(); if(r){ startProfileIdx+=(r>0?1:-1); if(startProfileIdx<0) startProfileIdx=n-1; if(startProfileIdx>=n) startProfileIdx=0; }
if(startProfileIdx<topIndex) topIndex=startProfileIdx; if(startProfileIdx>=topIndex+4) topIndex=startProfileIdx-3;
for(int row=0; row<4; row++){
int i=topIndex+row; if(i>=n) break;
String line="["+String(i+1)+"] "; line+=chemToStr(profiles[i].chem);
line+=" S="+String(profiles[i].S); line+=" "+String(profiles[i].capacity_mAh)+"mAh";
drawMenuItem(12+(row+1)*11, line, i==startProfileIdx);
}
drawFooterHint("Нажми: правка | Держи: назад");
u8g2.sendBuffer();
if(isShortClick()){
Profile &p=profiles[startProfileIdx];
quickCap_mAh=p.capacity_mAh; quickRateC=p.rateC;
quickItermA=(p.ItermA>0.001f)?p.ItermA:((p.chem==CHEM_LIION?0.05f:0.02f)*p.capacity_mAh/1000.0f);
ui=UI_START_QUICK_CAP; saveLastIndex(startProfileIdx);
}
if(isLongPressReleased()){ ui=UI_START_MODE; cursor=0; }
}
void editNumberInt(uint16_t &val,uint16_t minV,uint16_t maxV,uint16_t step,const char* title){
u8g2.clearBuffer(); drawHeader(title);
int r=readRot(); if(r){ long nv=(long)val + (r>0?step:-step); if(nv<minV) nv=minV; if(nv>maxV) nv=maxV; val=(uint16_t)nv; }
setFont(); u8g2.setCursor(2, 22+10); u8g2.print(val); u8g2.print(" ");
drawFooterHint("Нажми: ОК | Держи: отмена");
u8g2.sendBuffer();
}
void editNumberFloat(float &val,float minV,float maxV,float step,const char* title){
u8g2.clearBuffer(); drawHeader(title);
int r=readRot(); if(r){ float nv=val+(r>0?step:-step); if(nv<minV) nv=minV; if(nv>maxV) nv=maxV; val=nv; }
setFont(); u8g2.setCursor(2, 22+10); u8g2.print(fmtFloat(val,(step<0.1f?2:1)));
drawFooterHint("Нажми: ОК | Держи: отмена");
u8g2.sendBuffer();
}
void screenStartQuickCap(){ editNumberInt(quickCap_mAh,100,10000,100,"Емкость (mAh)"); if(isShortClick()) ui=UI_START_QUICK_RATEC; if(isLongPressReleased()) ui=UI_START_PROFILE; }
void screenStartQuickRateC(){ editNumberFloat(quickRateC,0.1f,1.0f,0.1f,"C-rate"); if(isShortClick()) ui=UI_START_QUICK_ITERM; if(isLongPressReleased()) ui=UI_START_PROFILE; }
void screenStartQuickIterm(){ editNumberFloat(quickItermA,0.01f,5.0f,0.01f,"Iterm (A)"); if(isShortClick()) ui=UI_START_CONFIRM; if(isLongPressReleased()) ui=UI_START_PROFILE; }
void screenStartConfirm(){
u8g2.clearBuffer(); drawHeader("Подтверждение");
Profile &p=profiles[startProfileIdx];
setFont();
u8g2.setCursor(2,12+10); u8g2.print("Режим: "); u8g2.print(startMode==SM_CHARGE?"Заряд":(startMode==SM_DISCHARGE?"Разряд":"МиллиОм"));
u8g2.setCursor(2,22+10); u8g2.print("Профиль: "); u8g2.print(chemToStr(p.chem)); u8g2.print(" S="); u8g2.print(p.S);
u8g2.setCursor(2,32+10); u8g2.print("Cap="); u8g2.print(quickCap_mAh); u8g2.print("mAh C="); u8g2.print(fmtFloat(quickRateC,1));
u8g2.setCursor(2,42+10); u8g2.print("Iterm="); u8g2.print(fmtFloat(quickItermA,2)); u8g2.print("A");
drawFooterHint("Нажми: ПУСК | Держи: назад");
u8g2.sendBuffer();
if(isShortClick()){ ui=UI_MESSAGE; }
if(isLongPressReleased()){ ui=UI_START_PROFILE; }
}
void screenMessage(){
u8g2.clearBuffer(); drawHeader("Сообщение");
setFont();
u8g2.setCursor(2,14+8); u8g2.print("Старт (демо).");
u8g2.setCursor(2,24+8); u8g2.print("Дальше добавим");
u8g2.setCursor(2,34+8); u8g2.print("алгоритм и логи.");
drawFooterHint("Нажми: в меню");
u8g2.sendBuffer();
if(isShortClick() || isDoubleClick() || isLongPressReleased()){ ui=UI_HOME; cursor=0; }
}
// Профили
enum EditField { EF_CHEM=0, EF_S, EF_CAP, EF_RATEC, EF_ITERM, EF_SAVE, EF_CANCEL };
int profListCursor=0, editProfileIdx=0; EditField editField=EF_CHEM;
void screenProfilesList(){
u8g2.clearBuffer(); drawHeader("Профили");
int n=NUM_PROFILES; int r=readRot(); if(r){ profListCursor+=(r>0?1:-1); if(profListCursor<0) profListCursor=n-1; if(profListCursor>=n) profListCursor=0; }
if(profListCursor<topIndex) topIndex=profListCursor; if(profListCursor>=topIndex+4) topIndex=profListCursor-3;
for(int row=0; row<4; row++){
int i=topIndex+row; if(i>=n) break;
String line="["+String(i+1)+"] "; line+=chemToStr(profiles[i].chem);
line+=" S="+String(profiles[i].S); line+=" "+String(profiles[i].capacity_mAh)+"mAh";
drawMenuItem(12+(row+1)*11, line, i==profListCursor);
}
drawFooterHint("Нажми: редакт. | Держи: назад");
u8g2.sendBuffer();
if(isShortClick()){ editProfileIdx=profListCursor; editField=EF_CHEM; ui=UI_PROFILE_EDIT_FIELD; }
if(isLongPressReleased()){ ui=UI_HOME; cursor=0; }
}
void screenProfileEditField(){
u8g2.clearBuffer(); drawHeader("Редакт.профиля");
const char* items[]={"Химия","S (банок)","Емкость (mAh)","C-rate","Iterm (A)","Сохранить","Отмена"}; int n=7;
int r=readRot(); if(r){ int c=(int)editField + (r>0?1:-1); if(c<0) c=n-1; if(c>=n) c=0; editField=(EditField)c; }
for(int i=0;i<n;i++) drawMenuItem(12+(i+1)*11, items[i], i==(int)editField);
drawFooterHint("Нажми: выбрать | Держи: назад");
u8g2.sendBuffer();
if(isShortClick()){
switch(editField){
case EF_CHEM: ui=UI_EDIT_CHEM; break;
case EF_S: ui=UI_EDIT_S; break;
case EF_CAP: ui=UI_EDIT_CAP; break;
case EF_RATEC: ui=UI_EDIT_RATEC; break;
case EF_ITERM: ui=UI_EDIT_ITERM; break;
case EF_SAVE: saveProfile(editProfileIdx); ui=UI_PROFILES_LIST; break;
case EF_CANCEL:ui=UI_PROFILES_LIST; break;
}
}
if(isLongPressReleased()) ui=UI_PROFILES_LIST;
}
void screenEditChem(){
u8g2.clearBuffer(); drawHeader("Химия");
uint8_t &c=profiles[editProfileIdx].chem; int r=readRot(); if(r){ int v=(int)c+(r>0?1:-1); if(v<0) v=2; if(v>2) v=0; c=(uint8_t)v; }
setFont(); u8g2.setCursor(2, 22+10); u8g2.print(chemToStr(c));
drawFooterHint("Нажми: ОК | Держи: отмена");
u8g2.sendBuffer();
if(isShortClick()||isLongPressReleased()) ui=UI_PROFILE_EDIT_FIELD;
}
void screenEditS(){
u8g2.clearBuffer(); drawHeader("S (1..4)");
uint8_t &S=profiles[editProfileIdx].S; int r=readRot(); if(r){ int v=(int)S+(r>0?1:-1); if(v<1) v=4; if(v>4) v=1; S=(uint8_t)v; }
setFont(); u8g2.setCursor(2,22+10); u8g2.print("S="); u8g2.print(S);
drawFooterHint("Нажми: ОК | Держи: отмена");
u8g2.sendBuffer();
if(isShortClick()||isLongPressReleased()) ui=UI_PROFILE_EDIT_FIELD;
}
void screenEditCap(){ editNumberInt(profiles[editProfileIdx].capacity_mAh,100,10000,100,"Емкость (mAh)"); if(isShortClick()||isLongPressReleased()) ui=UI_PROFILE_EDIT_FIELD; }
void screenEditRateC(){ editNumberFloat(profiles[editProfileIdx].rateC,0.1f,1.0f,0.1f,"C-rate"); if(isShortClick()||isLongPressReleased()) ui=UI_PROFILE_EDIT_FIELD; }
void screenEditIterm(){ editNumberFloat(profiles[editProfileIdx].ItermA,0.01f,5.0f,0.01f,"Iterm (A)"); if(isShortClick()||isLongPressReleased()) ui=UI_PROFILE_EDIT_FIELD; }
// -------- SETTINGS & SERVICE (заглушки) --------
void screenSettings(){
u8g2.clearBuffer(); drawHeader("Параметры");
setFont();
u8g2.setCursor(2,14+8); u8g2.print("Позже: лог/экран/сеть");
drawFooterHint("Держи: назад");
u8g2.sendBuffer();
if(isShortClick()||isLongPressReleased()) ui=UI_HOME;
}
void screenService(){
u8g2.clearBuffer(); drawHeader("Сервис");
setFont();
u8g2.setCursor(2,14+8); u8g2.print("Позже: тесты/калибровка");
drawFooterHint("Держи: назад");
u8g2.sendBuffer();
if(isShortClick()||isLongPressReleased()) ui=UI_HOME;
}
// -------- SETUP/LOOP --------
void setup(){
Serial.begin(115200);
Wire.begin(21,22);
Wire.setClock(100000);
u8g2.begin();
u8g2.enableUTF8Print(); // <<< ключевое: печатать UTF-8 (русский)
u8g2.setI2CAddress(0x3C<<1); // 0x3C из сканера
u8g2.setBusClock(100000);
pinMode(ENC_A_PIN, INPUT_PULLUP);
pinMode(ENC_B_PIN, INPUT_PULLUP);
pinMode(ENC_BTN_PIN, INPUT_PULLUP);
encBtn.attach(ENC_BTN_PIN, INPUT_PULLUP);
encBtn.interval(5);
loadProfiles();
u8g2.clearBuffer();
drawHeader("SmartCharge UI");
setFont();
u8g2.setCursor(2,20); u8g2.print("Меню готово.");
u8g2.setCursor(2,30); u8g2.print("A=32 B=33 BTN=26");
u8g2.setCursor(2,40); u8g2.print("Крутить/Жать/Держать");
u8g2.sendBuffer();
delay(700);
ui=UI_HOME; cursor=0; topIndex=0;
}
void loop(){
pollInputs();
switch(ui){
case UI_HOME: screenHome(); break;
case UI_START_MODE: screenStartMode(); break;
case UI_START_PROFILE: screenStartProfile(); break;
case UI_START_QUICK_CAP: screenStartQuickCap(); break;
case UI_START_QUICK_RATEC: screenStartQuickRateC(); break;
case UI_START_QUICK_ITERM: screenStartQuickIterm(); break;
case UI_START_CONFIRM: screenStartConfirm(); break;
case UI_PROFILES_LIST: screenProfilesList(); break;
case UI_PROFILE_EDIT_FIELD: screenProfileEditField(); break;
case UI_EDIT_CHEM: screenEditChem(); break;
case UI_EDIT_S: screenEditS(); break;
case UI_EDIT_CAP: screenEditCap(); break;
case UI_EDIT_RATEC: screenEditRateC(); break;
case UI_EDIT_ITERM: screenEditIterm(); break;
case UI_SETTINGS: screenSettings(); break;
case UI_SERVICE: screenService(); break;
case UI_MESSAGE: screenMessage(); break;
}
delay(6);
}