#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// ===================== Display =====================
static constexpr uint8_t OLED_ADDR = 0x3C;
static constexpr int OLED_RESET = -1;
static constexpr int SCREEN_W = 128;
static constexpr int SCREEN_H = 64;
Adafruit_SSD1306 display(SCREEN_W, SCREEN_H, &Wire, OLED_RESET);
// ===================== Pins =====================
static constexpr uint8_t PIN_MACRO = 13;
static constexpr uint8_t PIN_PROFILE = 12;
// ===================== Debounce helper =====================
struct DebouncedButton {
uint8_t pin;
bool stableState; // stable (debounced) state
bool lastStableState;
bool lastReadState; // last raw read
uint32_t lastChangeMs;
uint16_t delayMs;
void begin(uint8_t p, uint16_t dly, bool pullup = true) {
pin = p;
delayMs = dly;
if (pullup) pinMode(pin, INPUT_PULLUP);
else pinMode(pin, INPUT);
bool r = digitalRead(pin);
stableState = r;
lastStableState = r;
lastReadState = r;
lastChangeMs = millis();
}
// returns true when stable state changed
bool update() {
bool r = digitalRead(pin);
if (r != lastReadState) {
lastReadState = r;
lastChangeMs = millis();
}
if ((millis() - lastChangeMs) >= delayMs && stableState != r) {
lastStableState = stableState;
stableState = r;
return true;
}
return false;
}
// "pressed" for INPUT_PULLUP is LOW
bool fell() const { return (lastStableState == HIGH && stableState == LOW); }
};
// ===================== Profiles =====================
struct Profile {
const char* name;
const char* app;
const char* tip;
const char* macro;
const char* serialCmd;
};
static const Profile profiles[] = {
{"Windows", "OS Control",
"Win+D: desktop\nAlt+Tab: switch apps",
"WIN+D", "SEND:SHOW_DESKTOP"},
{"Excel", "Spreadsheet",
"Ctrl+Shift+L: filters\nAlt+=: autosum",
"ALT+=", "SEND:AUTO_SUM"},
{"Zoom", "Video Call",
"Alt+A: mute/unmute\nAlt+V: video on/off",
"ALT+A", "SEND:MUTE_TOGGLE"},
{"Photoshop", "Graphics",
"B: brush tool\nCtrl+Z: undo",
"B", "SEND:BRUSH_TOOL"}
};
static constexpr int PROFILE_COUNT = sizeof(profiles) / sizeof(profiles[0]);
// ===================== State =====================
int currentProfile = 0;
bool connected = true;
DebouncedButton btnMacro;
DebouncedButton btnProfile;
bool screenDirty = true;
// ===================== UI helpers =====================
static void drawHeader() {
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print("AI MacroPad");
display.setCursor(78, 0);
display.print(connected ? "Ready" : "Offline");
display.setCursor(118, 0);
display.print(currentProfile + 1);
}
static void drawMultilineText(int x, int y, const char* text, int maxLines) {
// Simple newline-based renderer (fits SSD1306 nicely)
display.setCursor(x, y);
int lines = 0;
const char* p = text;
while (*p && lines < maxLines) {
// print until '\n' or end
const char* lineEnd = p;
while (*lineEnd && *lineEnd != '\n') lineEnd++;
// print this line
for (const char* c = p; c < lineEnd; c++) display.print(*c);
// move to next line
lines++;
if (*lineEnd == '\n') lineEnd++;
p = lineEnd;
if (*p && lines < maxLines) {
display.setCursor(x, y + lines * 10);
}
}
}
static void renderScreen() {
display.clearDisplay();
drawHeader();
display.setCursor(0, 14);
display.print("App: ");
display.print(profiles[currentProfile].app);
display.setCursor(0, 26);
display.print("Profile: ");
display.print(profiles[currentProfile].name);
display.setCursor(0, 38);
display.print("Macro: ");
display.print(profiles[currentProfile].macro);
display.setCursor(0, 50);
display.print("Tip:");
// show up to 1 line of tip on screen to keep UI clean
display.setCursor(26, 50);
// show first line only
const char* tip = profiles[currentProfile].tip;
while (*tip && *tip != '\n') {
display.print(*tip++);
}
display.display();
}
static void flashSendBadge() {
// small feedback in top-right area
display.fillRect(92, 0, 26, 10, SSD1306_BLACK);
display.setCursor(92, 0);
display.print("SEND");
display.display();
delay(180);
screenDirty = true;
}
// ===================== Logic =====================
static void doMacro() {
Serial.print("HID EMU: ");
Serial.println(profiles[currentProfile].serialCmd);
flashSendBadge();
// full tip in serial (as "AI assistant")
Serial.println("=== AI Tip ===");
Serial.print("For ");
Serial.print(profiles[currentProfile].app);
Serial.println(":");
Serial.println(profiles[currentProfile].tip);
Serial.println("=============");
}
static void nextProfile() {
currentProfile = (currentProfile + 1) % PROFILE_COUNT;
Serial.print("Profile -> ");
Serial.println(profiles[currentProfile].name);
screenDirty = true;
}
// ===================== Arduino =====================
void setup() {
Serial.begin(115200);
btnMacro.begin(PIN_MACRO, 50, true);
btnProfile.begin(PIN_PROFILE, 50, true);
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
Serial.println("SSD1306 init failed");
for (;;) {}
}
display.clearDisplay();
display.display();
Serial.println("=== AI MacroPad (ESP32) ===");
Serial.println("Mode: Serial HID emulation");
Serial.println("Buttons: macro / profile");
screenDirty = true;
}
void loop() {
// Update buttons; if changed, check for press event
if (btnMacro.update() && btnMacro.fell()) {
doMacro();
}
if (btnProfile.update() && btnProfile.fell()) {
nextProfile();
}
if (screenDirty) {
renderScreen();
screenDirty = false;
}
delay(5);
}