/* 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); }