From aa07904699963cbcd362eb0711f521b18ff68921 Mon Sep 17 00:00:00 2001 From: elios Date: Fri, 22 Aug 2025 17:10:45 +0000 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20firmware/test-menu.ino?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- firmware/test-menu.ino | 523 +++++++++++++++++++++++++++++++++++++++++ test-menu.ino | 446 ----------------------------------- 2 files changed, 523 insertions(+), 446 deletions(-) create mode 100644 firmware/test-menu.ino delete mode 100644 test-menu.ino diff --git a/firmware/test-menu.ino b/firmware/test-menu.ino new file mode 100644 index 0000000..25c1896 --- /dev/null +++ b/firmware/test-menu.ino @@ -0,0 +1,523 @@ +/* + SmartCharge UI (меню обновлённое) + идеальный энкодер на PCNT + МиллиОм (1S) + Изменения: + - Подсказки снизу только на главном экране. + - Старт→Профиль: "Быстрый старт" или "Изменить параметры". + - МиллиОм (1S): без профилей, один клик = измерение Voc/нагрузка (INA219 + FR120N). + Пины: + I2C OLED: SDA=21, SCL=22, addr 0x3C + Encoder: S1=GPIO32, S2=GPIO33, Key=GPIO26 (модуль "GND S1 S2 Key 5V" -> 3V3) + MOSFET: GPIO25 (FR120N модуль вход "PWM/EN") + DS18B20: GPIO16 (опционально) +*/ + +#define USE_SH1106 1 // 1=SH1106, 0=SSD1306 + +#include +#include +#include +#include "driver/pcnt.h" +#include +#include +#include + +// ---------- 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 + +// ---------- Пины ---------- +#define ENC_A 32 // S1 (A/pulse) +#define ENC_B 33 // S2 (B/dir) +#define ENC_SW 26 // кнопка +#define MOSFET_PIN 25 +#define ONEWIRE_PIN 16 + +// ---------- Энкодер/PCNT ---------- +#define ENC_DIR (+1) +#define DETENT_DIV 1 // 1 фронт = 1 шаг (самая отзывчивая) +#define FILTER_US 8 // аппаратный антидребезг PCNT (~макс 12.8 мкс) + +// Кнопка +bool btnPressed=false, btnShort=false, btnLongRel=false; +uint32_t btnDownAt=0; const uint32_t BTN_DB_MS=15, BTN_LONG_MS=700; + +void buttonUpdate(){ + static bool lastRaw=true; static uint32_t lastT=0; + bool raw = digitalRead(ENC_SW); // PULLUP: 1=отпущена, 0=нажата + uint32_t now=millis(); + if (raw!=lastRaw){ lastRaw=raw; lastT=now; } + if (now-lastT < BTN_DB_MS) return; + if (!raw && !btnPressed){ btnPressed=true; btnShort=false; btnLongRel=false; btnDownAt=now; } + else if (raw && btnPressed){ btnPressed=false; if(now-btnDownAt>=BTN_LONG_MS) btnLongRel=true; else btnShort=true; } +} +inline bool isClick(){ if(btnShort){btnShort=false; return true;} return false; } +inline bool isHold(){ if(btnLongRel){btnLongRel=false; return true;} return false; } + +void pcntInit(){ + pcnt_config_t cfg = {}; + cfg.pulse_gpio_num = (gpio_num_t)ENC_A; + cfg.ctrl_gpio_num = (gpio_num_t)ENC_B; + cfg.unit = PCNT_UNIT_0; + cfg.channel = PCNT_CHANNEL_0; + cfg.pos_mode = PCNT_COUNT_INC; // фронт A -> +1 + cfg.neg_mode = PCNT_COUNT_DIS; // спад игнорируем + cfg.lctrl_mode = PCNT_MODE_REVERSE; // B=LOW -> инверсия + cfg.hctrl_mode = PCNT_MODE_KEEP; // B=HIGH -> keep + pcnt_unit_config(&cfg); + + uint16_t ticks = (uint16_t)min(1023, (int)(FILTER_US * 80)); + pcnt_set_filter_value(PCNT_UNIT_0, ticks); + pcnt_filter_enable(PCNT_UNIT_0); + + pcnt_counter_pause(PCNT_UNIT_0); + pcnt_counter_clear(PCNT_UNIT_0); + pcnt_counter_resume(PCNT_UNIT_0); +} + +long readDetents(){ + static long accum = 0; + int16_t cnt=0; + pcnt_get_counter_value(PCNT_UNIT_0, &cnt); + if (cnt) pcnt_counter_clear(PCNT_UNIT_0); + accum += (long)cnt * ENC_DIR; + long steps = accum / DETENT_DIV; + if (steps) accum -= steps * DETENT_DIV; + return steps; +} +int readRot(){ + long d = readDetents(); + if (!d) return 0; + if (d > 3) d = 3; if (d < -3) d = -3; + return (int)d; +} + +// ---------- Сенсоры ---------- +Adafruit_INA219 ina219; +OneWire oneWire(ONEWIRE_PIN); +DallasTemperature dallas(&oneWire); + +// ---------- Профили/меню ---------- +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; + +enum UiState { + UI_HOME=0, + UI_START_MODE, UI_START_PROFILE, UI_START_ACTION, + UI_START_QUICK_CAP, UI_START_QUICK_RATEC, UI_START_QUICK_ITERM, UI_START_CONFIRM, + UI_RINT_READY, + 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; + +// МиллиОм параметры (простые дефолты) +uint16_t rintPulseMs = 250; +uint8_t rintSamples = 12; +float rLeads_mOhm = 0.0f; +bool rintHasResult=false; +float rint_Voc=0, rint_Vload=0, rint_I=0, rint_RmOhm=0, rint_Tc= NAN; + +// ---------- отрисовка ---------- +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); } +} + +// ---------- profiles load/save ---------- +void loadProfiles(){ + prefs.begin("sc", true); + for(int i=0;i=n) cursor=0; } + for(int i=0;i=n) cursor=0; } + for(int i=0;i=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)+" "+String(profiles[i].capacity_mAh)+"mAh"; + drawMenuItem(12+(row+1)*11, line, i==startProfileIdx); + } + // Ни подсказок + u8g2.sendBuffer(); + + if(isClick()){ + // после выбора профиля — экран действия: быстрый старт или правка параметров + ui=UI_START_ACTION; saveLastIndex(startProfileIdx); + } + if(isHold()){ ui=UI_START_MODE; cursor=0; } +} + +void screenStartAction(){ + u8g2.clearBuffer(); drawHeader("Действие"); + const char* items[]={"Быстрый старт","Изменить параметры"}; int n=2; + int r=readRot(); if(r){ cursor += r; if(cursor<0) cursor=n-1; if(cursor>=n) cursor=0; } + for(int i=0;i0.001f)?p.ItermA:((p.chem==CHEM_LIION?0.05f:0.02f)*p.capacity_mAh/1000.0f); + ui=UI_START_QUICK_CAP; + } + } + if(isHold()){ ui=UI_START_PROFILE; } +} + +// Быстрые параметры +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; } + u8g2.setCursor(2, 22+10); u8g2.print(val); u8g2.print(" "); + 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; } + char b[16]; dtostrf(val,0,(step<0.1f?2:1),b); + u8g2.setCursor(2, 22+10); u8g2.print(b); + u8g2.sendBuffer(); +} +void screenStartQuickCap(){ editNumberInt(quickCap_mAh,100,10000,100,"Емкость (mAh)"); if(isClick()){ ui=UI_START_QUICK_RATEC; } if(isHold()){ ui=UI_START_ACTION; } } +void screenStartQuickRateC(){ editNumberFloat(quickRateC,0.1f,1.0f,0.1f,"C-rate"); if(isClick()){ ui=UI_START_QUICK_ITERM; } if(isHold()){ ui=UI_START_ACTION; } } +void screenStartQuickIterm(){ editNumberFloat(quickItermA,0.01f,5.0f,0.01f,"Iterm (A)"); if(isClick()){ ui=UI_START_CONFIRM; } if(isHold()){ ui=UI_START_ACTION; } } + +void screenStartConfirm(){ + u8g2.clearBuffer(); drawHeader("Подтверждение"); + Profile &p=profiles[startProfileIdx]; + u8g2.setCursor(2,22); u8g2.print("Режим: "); u8g2.print(startMode==SM_CHARGE?"Заряд":"Разряд"); + 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"); + u8g2.sendBuffer(); + + if(isClick()){ ui=UI_MESSAGE; } // сюда позже прикрутим реальный старт + if(isHold()){ ui=UI_START_ACTION; } +} + +// --- МиллиОм (1S) --- +float avgMany(std::function f, uint8_t n){ + double s=0; for(uint8_t i=0;i 150) Tc = NAN; + + // 3) расчёт + float dV = Voc - Vload; + float R = (Iload > 0.01f) ? (dV / Iload) : 0.0f; // Ом + float Rm = R*1000.0f - rLeads_mOhm; if (Rm < 0) Rm = 0; + + rint_Voc=Voc; rint_Vload=Vload; rint_I=Iload; rint_RmOhm=Rm; rint_Tc=Tc; rintHasResult=true; +} + +void screenRintReady(){ + u8g2.clearBuffer(); drawHeader("МиллиОм (1S)"); + u8g2.setCursor(2,22); u8g2.print("Клик: измерить"); + u8g2.setCursor(2,33); u8g2.print("Pulse="); u8g2.print(rintPulseMs); u8g2.print("ms N="); u8g2.print(rintSamples); + u8g2.setCursor(2,44); u8g2.print("R_leads="); u8g2.print(rLeads_mOhm,1); u8g2.print(" mОм"); + if (rintHasResult){ + u8g2.setCursor(2,55); + if (rint_I < 0.2f) { u8g2.print("I низкий, проверь контакты"); } + else { /* ничего */ } + } + u8g2.sendBuffer(); + + if(isClick()){ + runRintOnce(); + // сразу показать результат: + u8g2.clearBuffer(); drawHeader("Rвнутр (итог)"); + u8g2.setCursor(2,22); u8g2.print("Rint="); + u8g2.print(rint_RmOhm,1); u8g2.print(" мОм"); + u8g2.setCursor(2,33); u8g2.print("Voc="); u8g2.print(rint_Voc,3); u8g2.print("В"); + u8g2.setCursor(2,44); u8g2.print("Vld="); u8g2.print(rint_Vload,3); u8g2.print("В I="); + u8g2.print(rint_I,3); u8g2.print("А"); + if (!isnan(rint_Tc)){ u8g2.setCursor(2,55); u8g2.print("T="); u8g2.print(rint_Tc,1); u8g2.print("C"); } + u8g2.sendBuffer(); + delay(900); // чуть задержим, чтоб было видно результат + } + if(isHold()){ ui=UI_START_MODE; } +} + +// --- Профили --- +int profListCursor=0, editProfileIdx=0; +enum EditField { EF_CHEM=0, EF_S, EF_CAP, EF_RATEC, EF_ITERM, EF_SAVE, EF_CANCEL }; +EditField editField=EF_CHEM; + +void screenProfilesList(){ + u8g2.clearBuffer(); drawHeader("Профили"); + int n=NUM_PROFILES; int r=readRot(); if(r){ profListCursor += r; 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)+" "+String(profiles[i].capacity_mAh)+"mAh"; + drawMenuItem(12+(row+1)*11, line, i==profListCursor); + } + u8g2.sendBuffer(); + + if(isClick()){ editProfileIdx=profListCursor; editField=EF_CHEM; ui=UI_PROFILE_EDIT_FIELD; } + if(isHold()){ 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; if(c<0) c=n-1; if(c>=n) c=0; editField=(EditField)c; } + for(int i=0;i2) v=0; c=(uint8_t)v; } + u8g2.setCursor(2, 22+10); u8g2.print(chemToStr(c)); + u8g2.sendBuffer(); + if(isClick()||isHold()) { 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; if(v<1) v=4; if(v>4) v=1; S=(uint8_t)v; } + u8g2.setCursor(2,22+10); u8g2.print("S="); u8g2.print(S); + u8g2.sendBuffer(); + if(isClick()||isHold()) { ui=UI_PROFILE_EDIT_FIELD; } +} +void screenEditCap(){ + u8g2.clearBuffer(); drawHeader("Емкость (mAh)"); + int r=readRot(); if(r){ long nv=(long)profiles[editProfileIdx].capacity_mAh + (r>0?100:-100); + if(nv<100) nv=100; if(nv>10000) nv=10000; profiles[editProfileIdx].capacity_mAh=(uint16_t)nv; } + u8g2.setCursor(2,22+10); u8g2.print(profiles[editProfileIdx].capacity_mAh); + u8g2.sendBuffer(); + if(isClick()||isHold()) { ui=UI_PROFILE_EDIT_FIELD; } +} +void screenEditRateC(){ + u8g2.clearBuffer(); drawHeader("C-rate"); + int r=readRot(); if(r){ float nv=profiles[editProfileIdx].rateC+(r>0?0.1f:-0.1f); + if(nv<0.1f) nv=0.1f; if(nv>1.0f) nv=1.0f; profiles[editProfileIdx].rateC=nv; } + u8g2.setCursor(2,22+10); u8g2.print(profiles[editProfileIdx].rateC,1); + u8g2.sendBuffer(); + if(isClick()||isHold()) { ui=UI_PROFILE_EDIT_FIELD; } +} +void screenEditIterm(){ + u8g2.clearBuffer(); drawHeader("Iterm (A)"); + int r=readRot(); if(r){ float nv=profiles[editProfileIdx].ItermA+(r>0?0.01f:-0.01f); + if(nv<0.01f) nv=0.01f; if(nv>5.0f) nv=5.0f; profiles[editProfileIdx].ItermA=nv; } + u8g2.setCursor(2,22+10); u8g2.print(profiles[editProfileIdx].ItermA,2); + u8g2.sendBuffer(); + if(isClick()||isHold()) { ui=UI_PROFILE_EDIT_FIELD; } +} + +// --- Настройки/Сервис (заглушки) --- +void screenSettings(){ + u8g2.clearBuffer(); drawHeader("Параметры"); + u8g2.setCursor(2,26); u8g2.print("Позже: сеть/лог/экран"); + u8g2.sendBuffer(); + if(isClick()||isHold()) { ui=UI_HOME; cursor=0; } +} +void screenService(){ + u8g2.clearBuffer(); drawHeader("Сервис"); + u8g2.setCursor(2,26); u8g2.print("Позже: тесты/калибр."); + u8g2.sendBuffer(); + if(isClick()||isHold()) { ui=UI_HOME; cursor=0; } +} + +// --- Сообщение (заглушка запуска charge/discharge) --- +void screenMessage(){ + u8g2.clearBuffer(); drawHeader("Сообщение"); + u8g2.setCursor(2,26); + if (startMode==SM_CHARGE) u8g2.print("Старт заряда (демо)"); + else u8g2.print("Старт разряда (демо)"); + u8g2.setCursor(2,38); u8g2.print("Дальше прикрутим логику"); + u8g2.sendBuffer(); + if(isClick()||isHold()){ ui=UI_HOME; cursor=0; } +} + +// ---------- 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, INPUT_PULLUP); + pinMode(ENC_B, INPUT_PULLUP); + pinMode(ENC_SW, INPUT_PULLUP); + pcntInit(); + + pinMode(MOSFET_PIN, OUTPUT); + digitalWrite(MOSFET_PIN, LOW); + + if (!ina219.begin()){ + Serial.println("INA219 not found!"); + } + dallas.begin(); + + loadProfiles(); + + u8g2.clearBuffer(); drawHeader("SmartCharge UI"); + u8g2.setCursor(2,20); u8g2.print("OLED ок. PCNT ок."); + u8g2.setCursor(2,32); u8g2.print("S1=32 S2=33 Key=26"); + u8g2.setCursor(2,44); u8g2.print("Клик для входа"); + u8g2.sendBuffer(); + delay(600); +} + +void loop(){ + buttonUpdate(); + + switch(ui){ + case UI_HOME: screenHome(); break; + case UI_START_MODE: screenStartMode(); break; + case UI_START_PROFILE: screenStartProfile(); break; + case UI_START_ACTION: screenStartAction(); 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_RINT_READY: screenRintReady(); 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; + } +} diff --git a/test-menu.ino b/test-menu.ino deleted file mode 100644 index c8832d2..0000000 --- a/test-menu.ino +++ /dev/null @@ -1,446 +0,0 @@ -/* - 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 -#include -#include - -// ------------ 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;i0)? +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)+" "+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(nvmaxV) 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(nvmaxV) 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+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;i0?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); -}