diff --git a/test-menu.ino b/test-menu.ino new file mode 100644 index 0000000..b9ec52b --- /dev/null +++ b/test-menu.ino @@ -0,0 +1,409 @@ +/* + 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 +#include +#include +#include + +// -------- 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;i150){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;i0?1:-1); if(cursor<0) cursor=n-1; if(cursor>=n) cursor=0; } + for(int i=0;i0?1:-1); if(startProfileIdx<0) startProfileIdx=n-1; if(startProfileIdx>=n) startProfileIdx=0; } + 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(nvmaxV) 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(nvmaxV) 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+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;i0?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); +}