smart_chargeESP32/test-menu.ino

446 lines
18 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 - Меню (ESP32 + OLED + Encoder) [U8g2 + стабильный энкодер]
- Драйвер OLED: USE_SH1106=1 для SH1106, 0 для SSD1306
- I2C: SDA=21, SCL=22, адрес 0x3C (проверен сканером)
- Энкодер: A=GPIO32, B=GPIO33, BTN=GPIO26 (все с INPUT_PULLUP)
- Подключение энкодера: общий (COM) -> GND; A -> 32; B -> 33.
Кнопка: один контакт -> GND, второй -> 26.
- Рекомендация: к A и B поставить по 10нФ на землю для тихого вращения.
*/
#define USE_SH1106 1 // 1 = SH1106, 0 = SSD1306
#include <Wire.h>
#include <Preferences.h>
#include <U8g2lib.h>
// ------------ OLED ------------
#if USE_SH1106
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
#else
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
#endif
#define MENU_FONT u8g2_font_6x12_t_cyrillic
// ------------ ПИНЫ ЭНКОДЕРА ------------
const int ENC_A_PIN = 32;
const int ENC_B_PIN = 33;
const int ENC_BTN_PIN = 26;
// ------------ УСТОЙЧИВАЯ ОБРАБОТКА ЭНКОДЕРА ------------
// Алгоритм: принимаем только валидные переходы и считаем «полный шаг» = 4 тика.
// Плюс фильтр по времени, чтобы отсечь дребезг (микросекунды).
volatile int16_t encSteps = 0; // целые клики (детенты)
static uint8_t lastAB = 0;
static int8_t accum = 0;
static uint32_t lastEdgeUs = 0;
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 };
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;
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;
// --- 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 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); }
}
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); }
// --- rotary input wrapper
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);
drawFooter("Нажми: выбор | Держи: назад");
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);
drawFooter("Нажми: далее | Держи: назад");
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)+" "+String(profiles[i].capacity_mAh)+"mAh";
drawMenuItem(12+(row+1)*11, line, i==startProfileIdx);
}
drawFooter("Нажми: правка | Держи: назад");
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; }
u8g2.setFont(MENU_FONT); u8g2.setCursor(2, 22+10); u8g2.print(val); u8g2.print(" ");
drawFooter("Нажми: ОК | Держи: отмена");
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; }
u8g2.setFont(MENU_FONT); u8g2.setCursor(2, 22+10); char b[16]; dtostrf(val,0,(step<0.1f?2:1),b); u8g2.print(b);
drawFooter("Нажми: ОК | Держи: отмена");
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];
u8g2.setFont(MENU_FONT);
u8g2.setCursor(2,22); u8g2.print("Режим: "); u8g2.print(startMode==SM_CHARGE?"Заряд":(startMode==SM_DISCHARGE?"Разряд":"МиллиОм"));
u8g2.setCursor(2,33); u8g2.print("Профиль: "); u8g2.print(chemToStr(p.chem)); u8g2.print(" S="); u8g2.print(p.S);
u8g2.setCursor(2,44); u8g2.print("Cap="); u8g2.print(quickCap_mAh); u8g2.print("mAh C="); u8g2.print(quickRateC,1);
u8g2.setCursor(2,55); u8g2.print("Iterm="); u8g2.print(quickItermA,2); u8g2.print("A");
drawFooter("Нажми: ПУСК | Держи: назад");
u8g2.sendBuffer();
if(isShortClick()){ ui=UI_MESSAGE; }
if(isLongPressReleased()){ ui=UI_START_PROFILE; }
}
void screenMessage(){
u8g2.clearBuffer(); drawHeader("Сообщение");
u8g2.setFont(MENU_FONT);
u8g2.setCursor(2,26); u8g2.print("Старт (демо).");
u8g2.setCursor(2,38); u8g2.print("Дальше: алгоритм/логи");
drawFooter("Нажми: в меню");
u8g2.sendBuffer();
if(isShortClick() || 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)+" "+String(profiles[i].capacity_mAh)+"mAh";
drawMenuItem(12+(row+1)*11, line, i==profListCursor);
}
drawFooter("Нажми: редакт. | Держи: назад");
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);
drawFooter("Нажми: выбрать | Держи: назад");
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; }
u8g2.setFont(MENU_FONT); u8g2.setCursor(2, 22+10); u8g2.print(chemToStr(c));
drawFooter("Нажми: ОК | Держи: отмена");
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; }
u8g2.setFont(MENU_FONT); u8g2.setCursor(2,22+10); u8g2.print("S="); u8g2.print(S);
drawFooter("Нажми: ОК | Держи: отмена");
u8g2.sendBuffer();
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 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 screenSettings(){
u8g2.clearBuffer(); drawHeader("Параметры");
u8g2.setFont(MENU_FONT);
u8g2.setCursor(2,26); u8g2.print("Позже: лог/экран/сеть");
drawFooter("Держи: назад");
u8g2.sendBuffer();
if(isShortClick()||isLongPressReleased()) ui=UI_HOME;
}
void screenService(){
u8g2.clearBuffer(); drawHeader("Сервис");
u8g2.setFont(MENU_FONT);
u8g2.setCursor(2,26); u8g2.print("Позже: тесты/калибровка");
drawFooter("Держи: назад");
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();
u8g2.setI2CAddress(0x3C << 1);
u8g2.setBusClock(100000);
pinMode(ENC_A_PIN, INPUT_PULLUP);
pinMode(ENC_B_PIN, INPUT_PULLUP);
pinMode(ENC_BTN_PIN, INPUT_PULLUP);
// Инициализируем lastAB текущим состояние контактов, чтобы избежать «скачка» при старте
lastAB = ((uint8_t)digitalRead(ENC_A_PIN) << 1) | (uint8_t)digitalRead(ENC_B_PIN);
loadProfiles();
// Экран приветствия + подсказка по пинам
u8g2.clearBuffer(); drawHeader("SmartCharge UI");
u8g2.setFont(MENU_FONT);
u8g2.setCursor(2,20); u8g2.print("Меню готово.");
u8g2.setCursor(2,32); u8g2.print("Encoder A=32 B=33 BTN=26");
u8g2.setCursor(2,44); u8g2.print("COM/BTN GND, pullups вкл.");
u8g2.sendBuffer();
delay(700);
}
void loop(){
// Регулярно опрашиваем энкодер и кнопку
encoderUpdate();
buttonUpdate();
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(1);
}