Обновить firmware/test-menu.ino
This commit is contained in:
parent
da37904f71
commit
aa07904699
2 changed files with 523 additions and 446 deletions
523
firmware/test-menu.ino
Normal file
523
firmware/test-menu.ino
Normal file
|
|
@ -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 <Wire.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
#include <U8g2lib.h>
|
||||||
|
#include "driver/pcnt.h"
|
||||||
|
#include <Adafruit_INA219.h>
|
||||||
|
#include <OneWire.h>
|
||||||
|
#include <DallasTemperature.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
|
||||||
|
|
||||||
|
// ---------- Пины ----------
|
||||||
|
#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<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(); }
|
||||||
|
|
||||||
|
// ---------- screens ----------
|
||||||
|
void screenHome(){
|
||||||
|
u8g2.clearBuffer(); drawHeader("Главное меню");
|
||||||
|
const char* items[]={"Старт","Профили","Параметры","Сервис"}; int n=4;
|
||||||
|
int r=readRot(); if(r){ cursor += r; 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);
|
||||||
|
// Подсказка снизу ТОЛЬКО здесь:
|
||||||
|
setFont(); u8g2.setCursor(0,62); u8g2.print("Крутить: выбор Клик: вход Держать: назад");
|
||||||
|
u8g2.sendBuffer();
|
||||||
|
|
||||||
|
if(isClick()){
|
||||||
|
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; 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);
|
||||||
|
u8g2.sendBuffer();
|
||||||
|
|
||||||
|
if(isClick()){
|
||||||
|
startMode=(StartMode)cursor;
|
||||||
|
if (startMode==SM_RINT) { ui=UI_RINT_READY; return; } // миллиОм — без профилей
|
||||||
|
ui=UI_START_PROFILE; startProfileIdx=lastProfileIndex;
|
||||||
|
}
|
||||||
|
if(isHold()){ ui=UI_HOME; cursor=0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
void screenStartProfile(){
|
||||||
|
u8g2.clearBuffer(); drawHeader("Старт: профиль");
|
||||||
|
int n=NUM_PROFILES; int r=readRot(); if(r){ startProfileIdx += r; 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);
|
||||||
|
}
|
||||||
|
// Ни подсказок
|
||||||
|
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;i<n;i++) drawMenuItem(12+(i+1)*11, items[i], i==cursor);
|
||||||
|
u8g2.sendBuffer();
|
||||||
|
|
||||||
|
if(isClick()){
|
||||||
|
if(cursor==0){
|
||||||
|
// Быстрый старт: сразу в "сообщение" (заглушка алгоритма)
|
||||||
|
ui=UI_MESSAGE;
|
||||||
|
}else{
|
||||||
|
// Правка параметров перед стартом:
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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(nv<minV) nv=minV; if(nv>maxV) 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(nv<minV) nv=minV; if(nv>maxV) 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<float()> f, uint8_t n){
|
||||||
|
double s=0; for(uint8_t i=0;i<n;i++){ s += f(); delay(2); } return (float)(s/n);
|
||||||
|
}
|
||||||
|
|
||||||
|
void runRintOnce(){
|
||||||
|
rintHasResult=false;
|
||||||
|
// 0) проверка наличия батареи
|
||||||
|
float Voc = avgMany([](){ return ina219.getBusVoltage_V(); }, rintSamples);
|
||||||
|
if (Voc < 2.5f){ // универсальный безопасный порог
|
||||||
|
rint_Voc=Voc; rint_Vload=Voc; rint_I=0; rint_RmOhm=0; rint_Tc=NAN; rintHasResult=true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) импульс нагрузки
|
||||||
|
digitalWrite(MOSFET_PIN, HIGH);
|
||||||
|
delay(40); // спад фронта
|
||||||
|
float Vload = avgMany([](){ return ina219.getBusVoltage_V(); }, rintSamples);
|
||||||
|
float Iload = avgMany([](){ return ina219.getCurrent_mA()/1000.0f; }, rintSamples);
|
||||||
|
delay(rintPulseMs-40);
|
||||||
|
digitalWrite(MOSFET_PIN, LOW);
|
||||||
|
|
||||||
|
// 2) температура (если датчик есть)
|
||||||
|
dallas.requestTemperatures();
|
||||||
|
float Tc = dallas.getTempCByIndex(0);
|
||||||
|
if (Tc < -100 || Tc > 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) 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);
|
||||||
|
}
|
||||||
|
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;i<n;i++) drawMenuItem(12+(i+1)*11, items[i], i==(int)editField);
|
||||||
|
u8g2.sendBuffer();
|
||||||
|
|
||||||
|
if(isClick()){
|
||||||
|
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(isHold()){ 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; if(v<0) v=2; if(v>2) 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
446
test-menu.ino
446
test-menu.ino
|
|
@ -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 <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);
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue