Обновить test-menu.ino

This commit is contained in:
elios 2025-08-21 17:45:30 +00:00
parent 5406404d2f
commit b72c2a96e4

View file

@ -1,57 +1,121 @@
/* /*
SmartCharge - Menu MVP (ESP32 + OLED + Encoder) [U8g2 + кириллица] SmartCharge - Меню (ESP32 + OLED + Encoder) [U8g2 + стабильный энкодер]
- I2C OLED @ 0x3C (сканер уже нашёл) - Драйвер OLED: USE_SH1106=1 для SH1106, 0 для SSD1306
- USE_SH1106: 1 = SH1106, 0 = SSD1306 - I2C: SDA=21, SCL=22, адрес 0x3C (проверен сканером)
- SDA=21, SCL=22 - Энкодер: A=GPIO32, B=GPIO33, BTN=GPIO26 (все с INPUT_PULLUP)
- Encoder: A=32, B=33, BTN=26 - Подключение энкодера: общий (COM) -> GND; A -> 32; B -> 33.
Кнопка: один контакт -> GND, второй -> 26.
- Рекомендация: к A и B поставить по 10нФ на землю для тихого вращения.
*/ */
#define USE_SH1106 1 // <<< если у тебя SSD1306, поставь 0 #define USE_SH1106 1 // 1 = SH1106, 0 = SSD1306
#include <Wire.h> #include <Wire.h>
#include <Preferences.h> #include <Preferences.h>
#include <Bounce2.h>
#include <U8g2lib.h> #include <U8g2lib.h>
// -------- OLED (U8g2) -------- // ------------ OLED ------------
#if USE_SH1106 #if USE_SH1106
// Полный буфер, аппаратный I2C U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /*reset=*/ U8X8_PIN_NONE);
#else #else
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /*reset=*/ U8X8_PIN_NONE); U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
#endif #endif
#define MENU_FONT u8g2_font_6x12_t_cyrillic
// Шрифт с кириллицей (компактный, читаемый) // ------------ ПИНЫ ЭНКОДЕРА ------------
#define MENU_FONT u8g2_font_6x12_t_cyrillic
// -------- ENCODER --------
const int ENC_A_PIN = 32; const int ENC_A_PIN = 32;
const int ENC_B_PIN = 33; const int ENC_B_PIN = 33;
const int ENC_BTN_PIN = 26; const int ENC_BTN_PIN = 26;
Bounce encBtn; // ------------ УСТОЙЧИВАЯ ОБРАБОТКА ЭНКОДЕРА ------------
volatile int16_t encDelta = 0; // Алгоритм: принимаем только валидные переходы и считаем «полный шаг» = 4 тика.
int8_t transTable[16] = { // Плюс фильтр по времени, чтобы отсечь дребезг (микросекунды).
0, -1, +1, 2, volatile int16_t encSteps = 0; // целые клики (детенты)
+1, 0, 2, -1, static uint8_t lastAB = 0;
-1, 2, 0, +1, static int8_t accum = 0;
2, +1, -1, 0 static uint32_t lastEdgeUs = 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 -------- const uint32_t ENC_DEBOUNCE_US = 500; // игнорируем переходы быстрее 0.5 мкс (защита от дребезга)
const int8_t trans[4][4] = {
/*from\to: 00 01 10 11 */
/*00*/ { 0, +1, -1, 0 },
/*01*/ { -1, 0, 0, +1 },
/*10*/ { +1, 0, 0, -1 },
/*11*/ { 0, -1, +1, 0 }
};
inline void encoderUpdate() {
// читаем сразу и A, и B (быстро)
uint8_t a = (uint8_t)digitalRead(ENC_A_PIN);
uint8_t b = (uint8_t)digitalRead(ENC_B_PIN);
uint8_t ab = (a << 1) | b; // 0..3
if (ab == lastAB) return; // нет изменений
uint32_t now = micros();
if (now - lastEdgeUs < ENC_DEBOUNCE_US) { // слишком быстро -> дребезг
lastAB = ab;
return;
}
lastEdgeUs = now;
int8_t d = trans[lastAB & 0x03][ab & 0x03];
lastAB = ab;
if (d == 0) return; // невалидный прыжок (дребезг)
accum += d;
// Полный шаг у обычного энкодера — 4 валидных перехода
if (accum >= +4) { encSteps++; accum = 0; }
else if (accum <= -4) { encSteps--; accum = 0; }
}
int16_t encoderReadAndClear() {
noInterrupts();
int16_t v = encSteps;
encSteps = 0;
interrupts();
return v;
}
// ------------ КНОПКА (простой дебаунс) ------------
bool btnPressed = false;
bool btnShortClick = false;
bool btnLongReleased = false;
uint32_t btnDownAt = 0;
const uint32_t BTN_DEBOUNCE_MS = 15;
const uint32_t BTN_LONG_MS = 800;
void buttonUpdate() {
static bool lastRaw = true; // INPUT_PULLUP -> покой = HIGH (true)
static uint32_t lastChange = 0;
bool raw = digitalRead(ENC_BTN_PIN); // HIGH=не нажата, LOW=нажата
uint32_t now = millis();
if (raw != lastRaw) { // изменение
lastRaw = raw;
lastChange = now;
}
// дебаунс: ждём стабильности
if (now - lastChange < BTN_DEBOUNCE_MS) return;
if (!raw && !btnPressed) { // нажали
btnPressed = true;
btnShortClick = false;
btnLongReleased = false;
btnDownAt = now;
} else if (raw && btnPressed) { // отпустили
btnPressed = false;
if (now - btnDownAt >= BTN_LONG_MS) btnLongReleased = true;
else btnShortClick = true;
}
}
bool isShortClick(){ if (btnShortClick){ btnShortClick=false; return true;} return false; }
bool isLongPressReleased(){ if (btnLongReleased){ btnLongReleased=false; return true;} return false; }
// ------------ ПРОФИЛИ/МЕНЮ (как раньше) ------------
enum : uint8_t { CHEM_LIION=0, CHEM_LFP=1, CHEM_LTO=2 }; enum : uint8_t { CHEM_LIION=0, CHEM_LFP=1, CHEM_LTO=2 };
static inline const char* chemToStr(uint8_t c){ 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"; } switch(c){ case CHEM_LIION: return "Li-ion"; case CHEM_LFP: return "LiFePO4"; case CHEM_LTO: return "LTO"; }
@ -64,6 +128,7 @@ struct Profile {
float rateC; // 0.1..1.0 float rateC; // 0.1..1.0
float ItermA; // A float ItermA; // A
}; };
Preferences prefs; Preferences prefs;
const int NUM_PROFILES = 5; const int NUM_PROFILES = 5;
Profile profiles[NUM_PROFILES]; Profile profiles[NUM_PROFILES];
@ -95,7 +160,7 @@ void saveProfile(int i) {
} }
void saveLastIndex(int idx){ prefs.begin("sc", false); prefs.putUChar("lastIdx", idx); prefs.end(); } void saveLastIndex(int idx){ prefs.begin("sc", false); prefs.putUChar("lastIdx", idx); prefs.end(); }
// -------- UI STATE -------- // UI state
enum UiState { enum UiState {
UI_HOME=0, UI_HOME=0,
UI_START_MODE, UI_START_PROFILE, UI_START_MODE, UI_START_PROFILE,
@ -107,90 +172,59 @@ enum UiState {
UiState ui = UI_HOME; UiState ui = UI_HOME;
int cursor=0, topIndex=0; 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 }; enum StartMode { SM_CHARGE=0, SM_DISCHARGE, SM_RINT };
StartMode startMode = SM_CHARGE; StartMode startMode = SM_CHARGE;
int startProfileIdx = 0; int startProfileIdx = 0;
uint16_t quickCap_mAh = 2500; float quickRateC=0.5f, quickItermA=0.10f; uint16_t quickCap_mAh = 2500; float quickRateC=0.5f, quickItermA=0.10f;
// -------- DRAW HELPERS (U8g2) -------- // --- helpers (U8g2)
void setFont(){ u8g2.setFont(MENU_FONT); } void setFont(){ u8g2.setFont(MENU_FONT); }
void drawHeader(const char* t){ void drawHeader(const char* t){
setFont(); setFont();
// белая плашка
u8g2.setDrawColor(1); u8g2.drawBox(0,0,128,12); u8g2.setDrawColor(1); u8g2.drawBox(0,0,128,12);
// чёрным по белому
u8g2.setDrawColor(0); u8g2.setCursor(2,10); u8g2.print(t); u8g2.setDrawColor(0); u8g2.setCursor(2,10); u8g2.print(t);
// вернуть белый цвет рисования
u8g2.setDrawColor(1); 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){ void drawMenuItem(int y, const String& text, bool sel){
setFont(); setFont();
if (sel){ if (sel){ u8g2.drawBox(0,y-9,128,11); u8g2.setDrawColor(0); u8g2.setCursor(2,y); u8g2.print(text); u8g2.setDrawColor(1); }
u8g2.drawBox(0,y-9,128,11); // выделенная строка else { u8g2.setCursor(2,y); u8g2.print(text); }
u8g2.setDrawColor(0); u8g2.setCursor(2,y); u8g2.print(text);
u8g2.setDrawColor(1);
} else {
u8g2.setCursor(2,y); u8g2.print(text);
}
} }
void drawFooter(const char* s){ setFont(); u8g2.setCursor(0,62); u8g2.print(s); }
String fmtFloat(float v,int p){ char b[20]; dtostrf(v,0,p,b); return String(b); } String fmtFloat(float v,int p){ char b[20]; dtostrf(v,0,p,b); return String(b); }
// -------- INPUT -------- // --- rotary input wrapper
void pollInputs(){ int readRot(int step=1){
encoderPoll(); int16_t d = encoderReadAndClear();
encBtn.update(); if (!d) return 0;
bool down = !encBtn.read(); // по одному пункту на «щелчок»
if (down && !btnWasDown) btnDownAt = ms(); return (d>0)? +step : -step;
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 -------- // --- screens
void screenHome(){ void screenHome(){
u8g2.clearBuffer(); u8g2.clearBuffer(); drawHeader("Главное меню");
drawHeader("Главное меню"); const char* items[]={"Старт","Профили","Параметры","Сервис"}; int n=4;
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; }
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); for(int i=0;i<n;i++) drawMenuItem(12+(i+1)*11, items[i], i==cursor);
drawFooterHint("Нажми: выбор | Держи: назад"); drawFooter("Нажми: выбор | Держи: назад");
u8g2.sendBuffer(); u8g2.sendBuffer();
if(isShortClick()){ if(isShortClick()){
if(cursor==0){ui=UI_START_MODE; cursor=0;} if(cursor==0){ ui=UI_START_MODE; cursor=0; }
else if(cursor==1){ui=UI_PROFILES_LIST; cursor=0; topIndex=0;} else if(cursor==1){ ui=UI_PROFILES_LIST; cursor=0; topIndex=0; }
else if(cursor==2){ui=UI_SETTINGS;} else if(cursor==2){ ui=UI_SETTINGS; }
else {ui=UI_SERVICE;} else { ui=UI_SERVICE; }
} }
} }
void screenStartMode(){ void screenStartMode(){
u8g2.clearBuffer(); u8g2.clearBuffer(); drawHeader("Старт: режим");
drawHeader("Старт: режим"); const char* items[]={"Заряд","Разряд (1S)","МиллиОм (1S)"}; int n=3;
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; }
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); for(int i=0;i<n;i++) drawMenuItem(12+(i+1)*11, items[i], i==cursor);
drawFooterHint("Нажми: далее | Держи: назад"); drawFooter("Нажми: далее | Держи: назад");
u8g2.sendBuffer(); u8g2.sendBuffer();
if(isShortClick()){ startMode=(StartMode)cursor; ui=UI_START_PROFILE; startProfileIdx=lastProfileIndex; } if(isShortClick()){ startMode=(StartMode)cursor; ui=UI_START_PROFILE; startProfileIdx=lastProfileIndex; }
@ -198,18 +232,18 @@ void screenStartMode(){
} }
void screenStartProfile(){ void screenStartProfile(){
u8g2.clearBuffer(); u8g2.clearBuffer(); drawHeader("Старт: профиль");
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; }
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) topIndex=startProfileIdx; if(startProfileIdx>=topIndex+4) topIndex=startProfileIdx-3; if(startProfileIdx>=topIndex+4) topIndex=startProfileIdx-3;
for(int row=0; row<4; row++){ for(int row=0; row<4; row++){
int i=topIndex+row; if(i>=n) break; int i=topIndex+row; if(i>=n) break;
String line="["+String(i+1)+"] "; line+=chemToStr(profiles[i].chem); String line="["+String(i+1)+"] "; line+=chemToStr(profiles[i].chem);
line+=" S="+String(profiles[i].S); line+=" "+String(profiles[i].capacity_mAh)+"mAh"; line+=" S="+String(profiles[i].S)+" "+String(profiles[i].capacity_mAh)+"mAh";
drawMenuItem(12+(row+1)*11, line, i==startProfileIdx); drawMenuItem(12+(row+1)*11, line, i==startProfileIdx);
} }
drawFooterHint("Нажми: правка | Держи: назад"); drawFooter("Нажми: правка | Держи: назад");
u8g2.sendBuffer(); u8g2.sendBuffer();
if(isShortClick()){ if(isShortClick()){
@ -224,15 +258,15 @@ void screenStartProfile(){
void editNumberInt(uint16_t &val,uint16_t minV,uint16_t maxV,uint16_t step,const char* title){ void editNumberInt(uint16_t &val,uint16_t minV,uint16_t maxV,uint16_t step,const char* title){
u8g2.clearBuffer(); drawHeader(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; } 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(" "); u8g2.setFont(MENU_FONT); u8g2.setCursor(2, 22+10); u8g2.print(val); u8g2.print(" ");
drawFooterHint("Нажми: ОК | Держи: отмена"); drawFooter("Нажми: ОК | Держи: отмена");
u8g2.sendBuffer(); u8g2.sendBuffer();
} }
void editNumberFloat(float &val,float minV,float maxV,float step,const char* title){ void editNumberFloat(float &val,float minV,float maxV,float step,const char* title){
u8g2.clearBuffer(); drawHeader(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; } 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))); u8g2.setFont(MENU_FONT); u8g2.setCursor(2, 22+10); char b[16]; dtostrf(val,0,(step<0.1f?2:1),b); u8g2.print(b);
drawFooterHint("Нажми: ОК | Держи: отмена"); drawFooter("Нажми: ОК | Держи: отмена");
u8g2.sendBuffer(); u8g2.sendBuffer();
} }
@ -243,12 +277,12 @@ void screenStartQuickIterm(){ editNumberFloat(quickItermA,0.01f,5.0f,0.01f,"Iter
void screenStartConfirm(){ void screenStartConfirm(){
u8g2.clearBuffer(); drawHeader("Подтверждение"); u8g2.clearBuffer(); drawHeader("Подтверждение");
Profile &p=profiles[startProfileIdx]; Profile &p=profiles[startProfileIdx];
setFont(); u8g2.setFont(MENU_FONT);
u8g2.setCursor(2,12+10); u8g2.print("Режим: "); u8g2.print(startMode==SM_CHARGE?"Заряд":(startMode==SM_DISCHARGE?"Разряд":"МиллиОм")); u8g2.setCursor(2,22); 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,33); 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,44); u8g2.print("Cap="); u8g2.print(quickCap_mAh); u8g2.print("mAh C="); u8g2.print(quickRateC,1);
u8g2.setCursor(2,42+10); u8g2.print("Iterm="); u8g2.print(fmtFloat(quickItermA,2)); u8g2.print("A"); u8g2.setCursor(2,55); u8g2.print("Iterm="); u8g2.print(quickItermA,2); u8g2.print("A");
drawFooterHint("Нажми: ПУСК | Держи: назад"); drawFooter("Нажми: ПУСК | Держи: назад");
u8g2.sendBuffer(); u8g2.sendBuffer();
if(isShortClick()){ ui=UI_MESSAGE; } if(isShortClick()){ ui=UI_MESSAGE; }
@ -257,14 +291,13 @@ void screenStartConfirm(){
void screenMessage(){ void screenMessage(){
u8g2.clearBuffer(); drawHeader("Сообщение"); u8g2.clearBuffer(); drawHeader("Сообщение");
setFont(); u8g2.setFont(MENU_FONT);
u8g2.setCursor(2,14+8); u8g2.print("Старт (демо)."); u8g2.setCursor(2,26); u8g2.print("Старт (демо).");
u8g2.setCursor(2,24+8); u8g2.print("Дальше добавим"); u8g2.setCursor(2,38); u8g2.print("Дальше: алгоритм/логи");
u8g2.setCursor(2,34+8); u8g2.print("алгоритм и логи."); drawFooter("Нажми: в меню");
drawFooterHint("Нажми: в меню");
u8g2.sendBuffer(); u8g2.sendBuffer();
if(isShortClick() || isDoubleClick() || isLongPressReleased()){ ui=UI_HOME; cursor=0; } if(isShortClick() || isLongPressReleased()){ ui=UI_HOME; cursor=0; }
} }
// Профили // Профили
@ -273,16 +306,16 @@ int profListCursor=0, editProfileIdx=0; EditField editField=EF_CHEM;
void screenProfilesList(){ void screenProfilesList(){
u8g2.clearBuffer(); drawHeader("Профили"); 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; } 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; if(profListCursor<topIndex) topIndex=profListCursor; if(profListCursor>=topIndex+4) topIndex=profListCursor-3;
for(int row=0; row<4; row++){ for(int row=0; row<4; row++){
int i=topIndex+row; if(i>=n) break; int i=topIndex+row; if(i>=n) break;
String line="["+String(i+1)+"] "; line+=chemToStr(profiles[i].chem); String line="["+String(i+1)+"] "; line+=chemToStr(profiles[i].chem);
line+=" S="+String(profiles[i].S); line+=" "+String(profiles[i].capacity_mAh)+"mAh"; line+=" S="+String(profiles[i].S)+" "+String(profiles[i].capacity_mAh)+"mAh";
drawMenuItem(12+(row+1)*11, line, i==profListCursor); drawMenuItem(12+(row+1)*11, line, i==profListCursor);
} }
drawFooterHint("Нажми: редакт. | Держи: назад"); drawFooter("Нажми: редакт. | Держи: назад");
u8g2.sendBuffer(); u8g2.sendBuffer();
if(isShortClick()){ editProfileIdx=profListCursor; editField=EF_CHEM; ui=UI_PROFILE_EDIT_FIELD; } if(isShortClick()){ editProfileIdx=profListCursor; editField=EF_CHEM; ui=UI_PROFILE_EDIT_FIELD; }
@ -294,7 +327,7 @@ void screenProfileEditField(){
const char* items[]={"Химия","S (банок)","Емкость (mAh)","C-rate","Iterm (A)","Сохранить","Отмена"}; int n=7; 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; } 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); for(int i=0;i<n;i++) drawMenuItem(12+(i+1)*11, items[i], i==(int)editField);
drawFooterHint("Нажми: выбрать | Держи: назад"); drawFooter("Нажми: выбрать | Держи: назад");
u8g2.sendBuffer(); u8g2.sendBuffer();
if(isShortClick()){ if(isShortClick()){
@ -314,8 +347,8 @@ void screenProfileEditField(){
void screenEditChem(){ void screenEditChem(){
u8g2.clearBuffer(); drawHeader("Химия"); 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; } 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)); u8g2.setFont(MENU_FONT); u8g2.setCursor(2, 22+10); u8g2.print(chemToStr(c));
drawFooterHint("Нажми: ОК | Держи: отмена"); drawFooter("Нажми: ОК | Держи: отмена");
u8g2.sendBuffer(); u8g2.sendBuffer();
if(isShortClick()||isLongPressReleased()) ui=UI_PROFILE_EDIT_FIELD; if(isShortClick()||isLongPressReleased()) ui=UI_PROFILE_EDIT_FIELD;
} }
@ -323,67 +356,70 @@ void screenEditChem(){
void screenEditS(){ void screenEditS(){
u8g2.clearBuffer(); drawHeader("S (1..4)"); 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; } 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); u8g2.setFont(MENU_FONT); u8g2.setCursor(2,22+10); u8g2.print("S="); u8g2.print(S);
drawFooterHint("Нажми: ОК | Держи: отмена"); drawFooter("Нажми: ОК | Держи: отмена");
u8g2.sendBuffer(); u8g2.sendBuffer();
if(isShortClick()||isLongPressReleased()) ui=UI_PROFILE_EDIT_FIELD; if(isShortClick()||isLongPressReleased()) ui=UI_PROFILE_EDIT_FIELD;
} }
void editNumberInt(uint16_t &val,uint16_t minV,uint16_t maxV,uint16_t step,const char* title);
void editNumberFloat(float &val,float minV,float maxV,float step,const char* title);
void screenEditCap(){ editNumberInt(profiles[editProfileIdx].capacity_mAh,100,10000,100,"Емкость (mAh)"); 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 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; } 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(){ void screenSettings(){
u8g2.clearBuffer(); drawHeader("Параметры"); u8g2.clearBuffer(); drawHeader("Параметры");
setFont(); u8g2.setFont(MENU_FONT);
u8g2.setCursor(2,14+8); u8g2.print("Позже: лог/экран/сеть"); u8g2.setCursor(2,26); u8g2.print("Позже: лог/экран/сеть");
drawFooterHint("Держи: назад"); drawFooter("Держи: назад");
u8g2.sendBuffer(); u8g2.sendBuffer();
if(isShortClick()||isLongPressReleased()) ui=UI_HOME; if(isShortClick()||isLongPressReleased()) ui=UI_HOME;
} }
void screenService(){ void screenService(){
u8g2.clearBuffer(); drawHeader("Сервис"); u8g2.clearBuffer(); drawHeader("Сервис");
setFont(); u8g2.setFont(MENU_FONT);
u8g2.setCursor(2,14+8); u8g2.print("Позже: тесты/калибровка"); u8g2.setCursor(2,26); u8g2.print("Позже: тесты/калибровка");
drawFooterHint("Держи: назад"); drawFooter("Держи: назад");
u8g2.sendBuffer(); u8g2.sendBuffer();
if(isShortClick()||isLongPressReleased()) ui=UI_HOME; if(isShortClick()||isLongPressReleased()) ui=UI_HOME;
} }
// -------- SETUP/LOOP -------- // ------------ SETUP/LOOP ------------
void setup(){ void setup(){
Serial.begin(115200); Serial.begin(115200);
Wire.begin(21,22); Wire.begin(21,22);
Wire.setClock(100000); Wire.setClock(100000);
u8g2.begin(); u8g2.begin();
u8g2.enableUTF8Print(); // <<< ключевое: печатать UTF-8 (русский) u8g2.enableUTF8Print();
u8g2.setI2CAddress(0x3C<<1); // 0x3C из сканера u8g2.setI2CAddress(0x3C << 1);
u8g2.setBusClock(100000); u8g2.setBusClock(100000);
pinMode(ENC_A_PIN, INPUT_PULLUP); pinMode(ENC_A_PIN, INPUT_PULLUP);
pinMode(ENC_B_PIN, INPUT_PULLUP); pinMode(ENC_B_PIN, INPUT_PULLUP);
pinMode(ENC_BTN_PIN, INPUT_PULLUP); pinMode(ENC_BTN_PIN, INPUT_PULLUP);
encBtn.attach(ENC_BTN_PIN, INPUT_PULLUP);
encBtn.interval(5); // Инициализируем lastAB текущим состояние контактов, чтобы избежать «скачка» при старте
lastAB = ((uint8_t)digitalRead(ENC_A_PIN) << 1) | (uint8_t)digitalRead(ENC_B_PIN);
loadProfiles(); loadProfiles();
u8g2.clearBuffer(); // Экран приветствия + подсказка по пинам
drawHeader("SmartCharge UI"); u8g2.clearBuffer(); drawHeader("SmartCharge UI");
setFont(); u8g2.setFont(MENU_FONT);
u8g2.setCursor(2,20); u8g2.print("Меню готово."); u8g2.setCursor(2,20); u8g2.print("Меню готово.");
u8g2.setCursor(2,30); u8g2.print("A=32 B=33 BTN=26"); u8g2.setCursor(2,32); u8g2.print("Encoder A=32 B=33 BTN=26");
u8g2.setCursor(2,40); u8g2.print("Крутить/Жать/Держать"); u8g2.setCursor(2,44); u8g2.print("COM/BTN GND, pullups вкл.");
u8g2.sendBuffer(); u8g2.sendBuffer();
delay(700); delay(700);
ui=UI_HOME; cursor=0; topIndex=0;
} }
void loop(){ void loop(){
pollInputs(); // Регулярно опрашиваем энкодер и кнопку
encoderUpdate();
buttonUpdate();
switch(ui){ switch(ui){
case UI_HOME: screenHome(); break; case UI_HOME: screenHome(); break;
@ -405,5 +441,6 @@ void loop(){
case UI_MESSAGE: screenMessage(); break; case UI_MESSAGE: screenMessage(); break;
} }
delay(6); // Без лишних задержек, чтобы не пропускать тики
delay(1);
} }