#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
/* ===================== OLED CONFIG ===================== */
static const int OLED_W = 128;
static const int OLED_H = 64;
static const int OLED_RST = -1; // обычно -1
static const uint8_t OLED_ADDR = 0x3C; // чаще всего 0x3C (иногда 0x3D)
Adafruit_SSD1306 oled(OLED_W, OLED_H, &Wire, OLED_RST);
/* ======================================================= */
/* ===================== PIN CONFIG ===================== */
static const uint8_t BTN1_PIN = 16; // Power
static const uint8_t BTN2_PIN = 17; // Action/System/Confirm
static const uint8_t BTN3_PIN = 18; // Nav/History
/* ======================================================= */
/* ===================== TIMING CONFIG ===================== */
static const uint32_t DEBOUNCE_MS = 45; // было 35
static const uint32_t DOUBLE_MS = 420; // было 320 (главный фикс)
static const uint32_t LONG_MS = 700; // было 650 (чуть спокойнее)
static const uint32_t REPEAT_MS = 220;
static const uint32_t BTN3_ARM_LEFT_MS = 420; // tap -> press&hold to scroll left
/* ========================================================= */
/* ===================== TYPES ===================== */
enum AppState : uint8_t {
ST_OFF = 0,
ST_ANSWER,
ST_CAPTURED,
ST_SYSTEM_MENU,
ST_HISTORY_MENU
};
enum BtnEvent : uint8_t {
EV_NONE = 0,
EV_CLICK,
EV_DOUBLE,
EV_LONG_START,
EV_LONG_REPEAT,
EV_LONG_RELEASE
};
struct Btn {
uint8_t pin = 0;
bool raw = false;
bool stable = false;
bool lastRaw = false;
bool lastStable = false;
uint32_t lastDebounceMs = 0;
uint32_t pressMs = 0;
bool longFired = false;
uint32_t lastRepeatMs = 0;
uint8_t clickCount = 0;
uint32_t lastClickReleaseMs = 0;
bool waitingSingle = false;
// BTN3 special
uint32_t armLeftUntilMs = 0;
bool leftModeActive = false;
};
/* ================================================= */
/* ===================== GLOBALS ===================== */
static AppState g_state = ST_OFF;
static const char* ANSWER_OPTIONS[] = { "Option A", "Option B", "Option C" };
static const uint8_t ANSWER_OPT_COUNT = sizeof(ANSWER_OPTIONS) / sizeof(ANSWER_OPTIONS[0]);
static const char* SYS_OPTIONS[] = { "WiFi", "Brightness", "About", "Reset" };
static const uint8_t SYS_OPT_COUNT = sizeof(SYS_OPTIONS) / sizeof(SYS_OPTIONS[0]);
static const char* HIST_ITEMS[] = { "Ans #1", "Ans #2", "Ans #3", "Ans #4" };
static const uint8_t HIST_COUNT = sizeof(HIST_ITEMS) / sizeof(HIST_ITEMS[0]);
static int g_sel = 0;
static int g_answerPage = 0;
static Btn b1, b2, b3;
static char g_lastAction[32] = "BOOT";
/* =================================================== */
/* ===================== HELPERS ===================== */
static void setLastAction(const char* s) {
strncpy(g_lastAction, s, sizeof(g_lastAction) - 1);
g_lastAction[sizeof(g_lastAction) - 1] = '\0';
}
static const char* stateToStr(AppState s) {
switch (s) {
case ST_OFF: return "OFF";
case ST_ANSWER: return "ANSWER";
case ST_CAPTURED: return "CAPTURED";
case ST_SYSTEM_MENU: return "SYSTEM";
case ST_HISTORY_MENU: return "HISTORY";
default: return "?";
}
}
static void drawOLED() {
oled.display();
oled.clearDisplay();
oled.setTextColor(SSD1306_WHITE);
// Header
oled.setTextSize(1);
oled.setCursor(0, 0);
oled.print("STATE: ");
oled.print(stateToStr(g_state));
// Last action
oled.setCursor(0, 10);
oled.print("ACT: ");
oled.print(g_lastAction);
// Body
oled.setCursor(0, 24);
if (g_state == ST_OFF) {
oled.print("BTN1: Power ON");
} else if (g_state == ST_ANSWER) {
oled.print("page=");
oled.print(g_answerPage);
oled.setCursor(0, 34);
oled.print("sel=");
oled.print(g_sel);
oled.print(" ");
oled.print(ANSWER_OPTIONS[g_sel]);
oled.setCursor(0, 48);
oled.print("B2=CAP B3=SEL");
} else if (g_state == ST_CAPTURED) {
oled.print("Photo ready (stub)");
oled.setCursor(0, 34);
oled.print("B2 click=SEND");
oled.setCursor(0, 44);
oled.print("B2 hold=CANCEL");
} else if (g_state == ST_SYSTEM_MENU) {
oled.print("sel=");
oled.print(g_sel);
oled.setCursor(0, 34);
oled.print(SYS_OPTIONS[g_sel]);
oled.setCursor(0, 48);
oled.print("B3=NEXT B2=OK");
} else if (g_state == ST_HISTORY_MENU) {
oled.print("sel=");
oled.print(g_sel);
oled.setCursor(0, 34);
oled.print(HIST_ITEMS[g_sel]);
oled.setCursor(0, 48);
oled.print("B3=NEXT B2=OK");
}
oled.display();
}
static void setState(AppState next) {
if (g_state == next) return;
if (next == ST_ANSWER || next == ST_SYSTEM_MENU || next == ST_HISTORY_MENU || next == ST_CAPTURED) {
g_sel = 0;
}
g_state = next;
drawOLED();
}
static bool readPressed(uint8_t pin) {
return (digitalRead(pin) == LOW);
}
static BtnEvent updateButton(Btn &b) {
const uint32_t now = millis();
b.raw = readPressed(b.pin);
if (b.raw != b.lastRaw) {
b.lastDebounceMs = now;
b.lastRaw = b.raw;
}
if ((now - b.lastDebounceMs) >= DEBOUNCE_MS) {
b.stable = b.raw;
}
// stable edge
if (b.stable != b.lastStable) {
b.lastStable = b.stable;
if (b.stable) {
b.pressMs = now;
b.longFired = false;
b.lastRepeatMs = now;
} else {
if (b.longFired) return EV_LONG_RELEASE;
b.clickCount++;
b.lastClickReleaseMs = now;
b.waitingSingle = true;
}
}
if (b.stable) {
const uint32_t held = now - b.pressMs;
if (!b.longFired && held >= LONG_MS) {
b.longFired = true;
return EV_LONG_START;
}
if (b.longFired && (now - b.lastRepeatMs) >= REPEAT_MS) {
b.lastRepeatMs = now;
return EV_LONG_REPEAT;
}
}
if (b.waitingSingle) {
if (b.clickCount >= 2) {
b.clickCount = 0;
b.waitingSingle = false;
return EV_DOUBLE;
}
if ((now - b.lastClickReleaseMs) > DOUBLE_MS) {
b.clickCount = 0;
b.waitingSingle = false;
return EV_CLICK;
}
}
return EV_NONE;
}
/* ===================== ACTIONS (STUBS) ===================== */
static void powerToggle() {
if (g_state == ST_OFF) {
setLastAction("Power ON");
setState(ST_ANSWER);
} else {
setLastAction("Power OFF");
setState(ST_OFF);
}
}
static void captureStub() {
setLastAction("YA SFOTKAL");
setState(ST_CAPTURED);
}
static void sendStub() {
setLastAction("OTPRAVIL");
setState(ST_ANSWER);
}
static void cancelStub() {
setLastAction("OTMENA");
setState(ST_ANSWER);
}
static void confirmStub() {
setLastAction("CONFIRM");
drawOLED();
}
static void cycleSel(uint8_t maxCount) {
if (!maxCount) return;
g_sel = (g_sel + 1) % maxCount;
setLastAction("SEL NEXT");
drawOLED();
}
static void toggleSystemMenu() {
if (g_state == ST_SYSTEM_MENU) {
setLastAction("SYS CLOSE");
setState(ST_ANSWER);
} else {
setLastAction("SYS OPEN");
setState(ST_SYSTEM_MENU);
}
}
static void toggleHistoryMenu() {
if (g_state == ST_HISTORY_MENU) {
setLastAction("HIST CLOSE");
setState(ST_ANSWER);
} else {
setLastAction("HIST OPEN");
setState(ST_HISTORY_MENU);
}
}
static void scrollRight() {
g_answerPage++;
setLastAction("SCROLL R");
drawOLED();
}
static void scrollLeft() {
g_answerPage--;
setLastAction("SCROLL L");
drawOLED();
}
/* ===================== ROUTING (YOUR RULES) ===================== */
static void handleBtn1(BtnEvent ev) {
if (ev == EV_CLICK) powerToggle();
}
static void handleBtn2(BtnEvent ev) {
if (g_state == ST_OFF) return;
// BTN2 double: toggle system menu (open from ANSWER, close from SYSTEM)
if (ev == EV_DOUBLE) {
if (g_state == ST_ANSWER || g_state == ST_SYSTEM_MENU) {
toggleSystemMenu();
}
return;
}
// BTN2 hold: only in CAPTURED cancel
if (ev == EV_LONG_START) {
if (g_state == ST_CAPTURED) cancelStub();
return;
}
// BTN2 click:
if (ev == EV_CLICK) {
if (g_state == ST_ANSWER) captureStub();
else if (g_state == ST_CAPTURED) sendStub();
else if (g_state == ST_SYSTEM_MENU || g_state == ST_HISTORY_MENU) confirmStub();
return;
}
}
static void handleBtn3(BtnEvent ev) {
if (g_state == ST_OFF) return;
// BTN3 double: toggle history menu (open from ANSWER, close from HISTORY)
if (ev == EV_DOUBLE) {
if (g_state == ST_ANSWER || g_state == ST_HISTORY_MENU) {
toggleHistoryMenu();
}
return;
}
// BTN3 click: in system/history switch variants; also allowed "everywhere" if ты хочешь
if (ev == EV_CLICK) {
if (g_state == ST_SYSTEM_MENU) cycleSel(SYS_OPT_COUNT);
else if (g_state == ST_HISTORY_MENU) cycleSel(HIST_COUNT);
else if (g_state == ST_ANSWER) {
// по твоему прошлому ТЗ: click везде может переключать варианты,
// но если тебе нужно, чтобы в ANSWER click не трогал выбор — скажешь.
cycleSel(ANSWER_OPT_COUNT);
// arm left gesture
b3.armLeftUntilMs = millis() + BTN3_ARM_LEFT_MS;
b3.leftModeActive = false;
}
return;
}
// BTN3 hold: only in ANSWER scroll right; tap->hold scroll left
if (g_state == ST_ANSWER) {
if (ev == EV_LONG_START) {
uint32_t now = millis();
if (now <= b3.armLeftUntilMs) {
b3.leftModeActive = true;
scrollLeft();
} else {
b3.leftModeActive = false;
scrollRight();
}
return;
}
if (ev == EV_LONG_REPEAT) {
if (b3.leftModeActive) scrollLeft();
else scrollRight();
return;
}
}
}
/* ===================== SETUP/LOOP ===================== */
void setup() {
Serial.begin(115200);
pinMode(BTN1_PIN, INPUT_PULLUP);
pinMode(BTN2_PIN, INPUT_PULLUP);
pinMode(BTN3_PIN, INPUT_PULLUP);
b1.pin = BTN1_PIN;
b2.pin = BTN2_PIN;
b3.pin = BTN3_PIN;
// I2C init (ESP32 defaults: SDA=21, SCL=22)
Wire.begin(21, 22);
if (!oled.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
// если экран не найден, просто пишем в Serial и зависаем
Serial.println("SSD1306 init failed");
while (true) delay(100);
}
oled.clearDisplay();
oled.display();
setLastAction("BOOT");
setState(ST_OFF);
}
void loop() {
BtnEvent e1 = updateButton(b1);
BtnEvent e2 = updateButton(b2);
BtnEvent e3 = updateButton(b3);
if (e1 != EV_NONE) handleBtn1(e1);
if (e2 != EV_NONE) handleBtn2(e2);
if (e3 != EV_NONE) handleBtn3(e3);
}