// ============================================================================
// EMAR EYES PERSONALITY FRAMEWORK (PWM REMOVED) - Arduino Nano B
// DESIGNED AND CREATED BY: MARVIN A. QUIZZAGAN, DECEMBER 7, 2025, 100% WORKING - PERFECT
// Features:
// - Supports multiple eye behaviors: DEFAULT, HEART, SMALL, SLEEPY, ANGRY,
// MARVIN, QUIZZAGAN, OFF, BLOCK, SCROLL (words).
// - Two 8x8 MAX7219 LED matrices as "eyes" (left/right) controlled via LedControl.
// - AUTO mode: runs "AUTO" marquee repeatedly and temporarily overrides other modes.
// - SCROLL mode: displays pre-defined text phrases with smooth scrolling; long-press A0 to enter/exit.
// - Mode button (A0) cycles behaviors; short press = next mode/scroll, long press = SCROLL mode.
// - External pause/resume via D13 pin (LOW = pause, HIGH = resume), preserves state.
// - D4 pin (LOW = AUTO) triggers AUTO mode, interrupts other activities.
// - Manual triggers via D2/D3 for left/right eye sequences (interrupt-based).
// - Smart delay mechanism allows responsive interrupts and mode changes during animations.
// - Eye patterns include open, wink, closed, directional, emotional (heart, sleepy, angry), letters, block, and blank.
// - Idle animation if no activity for 15s (bored animation).
// - Scroll text includes phrases like "GOOD MORNING", "I LOVE YOU", "GOD BLESS YOU", etc.
// - Priority-aware: AUTO and D13 pause have highest priority, interrupts normal operations.
// - Fully interruptible, safe to use with real-time changes.
// ============================================================================
#include <LedControl.h>
#include <Arduino.h>
// ------------------------ TUNABLE AUTO INDICATOR SETTINGS --------------------
unsigned long autoRunMs = 2500; // total ms for one running "AUTO" sequence
unsigned long autoIntervalMs = 8000; // ms between running "AUTO" re-flashes
// ------------------------ NEW TUNABLES: MARVIN / QUIZZAGAN -------------------
unsigned long marvinStepMs = 400; // per-step delay inside MARVIN sequence
unsigned long marvinPauseMs = 1200; // pause after MARVIN completes
unsigned long quizzaganStepMs = 400; // per-step delay inside QUIZZAGAN
unsigned long quizzaganPauseMs = 1200; // pause after QUIZZAGAN completes
// ----------------------------- MAX7219 PINS --------------------------------
const int DIN1 = 7;
const int CS1 = 6;
const int CLK1 = 5;
const int DIN2 = 12;
const int CS2 = 11;
const int CLK2 = 10;
LedControl lc1 = LedControl(DIN1, CLK1, CS1, 1); // LEFT eye
LedControl lc2 = LedControl(DIN2, CLK2, CS2, 1); // RIGHT eye
// ------------------------------ INPUT / SIGNAL PINS -------------------------
// D1 will be used as the indicator signal:
// LOW -> SCROLL WORDS mode active
// HIGH -> Normal eye animations / not-scrolling
// NOTE: D1 is Serial TX on Arduino Nano. Using it will affect Serial TX.
const int PIN_D1_SIGNAL = 1;
const int PIN_D2_LEFT = 2; // physical D2 (triggers RIGHT animation)
const int PIN_D3_RIGHT = 3; // physical D3 (triggers LEFT animation)
const int PIN_A0_MODE = A0; // behavior mode button (NO to GND, INPUT_PULLUP)
const int PIN_D4_AUTO = 4; // auto mode enable (LOW = auto)
const int PIN_D13_RESET = 13; // external pause (LOW = pause). Treated as priority pause.
// --------------------------- INTERRUPT FLAGS --------------------------------
volatile bool leftTriggered = false;
volatile bool rightTriggered = false;
// ----------------------------- MODES & STATES ------------------------------
enum BehaviorMode : uint8_t {
BEHAV_DEFAULT = 0,
BEHAV_HEART = 1,
BEHAV_SMALL = 2,
BEHAV_SLEEPY = 3,
BEHAV_ANGRY = 4,
BEHAV_MARVIN = 5,
BEHAV_QUIZZAGAN = 6,
BEHAV_OFF = 7, // fully disabled displays
BEHAV_BLOCK = 8, // steady 4x4 block
BEHAV_SCROLL = 9 // INTERNAL: Scroll Words mode (entered/exited by long-press A0)
};
BehaviorMode currentBehaviorMode = BEHAV_DEFAULT;
BehaviorMode previousBehaviorMode = BEHAV_DEFAULT; // store when entering scroll mode
// -------------------- Scroll Words state --------------------
const int NUM_SCROLLS = 9;
int scrollIndex = 0; // 0..NUM_SCROLLS-1 current selected scroll animation
bool exitScrollMode = false; // set when long-press exit requested
// --------------------------- BUTTON / DEBOUNCE ------------------------------
bool lastA0State = HIGH;
bool a0PressLatched = false; // short-press latch
bool a0LongLatched = false; // long-press latch
unsigned long a0LowStartTime = 0;
const unsigned long A0_DEBOUNCE_MS = 60;
const unsigned long A0_LONG_MS = 3000; // 3 seconds long press
unsigned long lastActivityMs = 0;
const unsigned long BORED_MS = 15000; // 15s idle -> bored animation
bool modeChanged = false;
// -------------------------- D13 / PAUSE STATE -------------------------------
bool pausedByD13 = false; // true when D13 forced a pause (LOW)
bool interruptedByAutoDuringScroll = false;
// ============================================================================
// EYE PATTERNS (8x8 bitmaps) - same as original
// ============================================================================
byte eyeOpen[8] = {
B00111100, B01111110, B11111111, B11100111,
B11100111, B11111111, B01111110, B00111100
};
byte eyeWink[8] = {
B00000000, B01111110, B11111111, B11100111,
B11100111, B11111111, B01111110, B00000000
};
byte eyeClosed[8] = {
B00000000, B00000000, B00000000, B11111111,
B11111111, B00000000, B00000000, B00000000
};
byte eyeClosedTotal[8] = {
B00000000, B00000000, B00000000, B00000000,
B11111111, B00000000, B00000000, B00000000
};
byte eyeRight1[8] = {
B00111100, B01111110, B11111111, B11001111,
B11001111, B11111111, B01111110, B00111100
};
byte eyeRight2[8] = {
B00111100, B01111110, B11111111, B10011111,
B10011111, B11111111, B01111110, B00111100
};
byte eyeRight3[8] = {
B00111100, B01111110, B11111111, B00111111,
B00111111, B11111111, B01111110, B00111100
};
byte eyeLeft1[8] = {
B00111100, B01111110, B11111111, B11110011,
B11110011, B11111111, B01111110, B00111100
};
byte eyeLeft2[8] = {
B00111100, B01111110, B11111111, B11111001,
B11111001, B11111111, B01111110, B00111100
};
byte eyeLeft3[8] = {
B00111100, B01111110, B11111111, B11111100,
B11111100, B11111111, B01111110, B00111100
};
byte eyeHeart1[8] = {
B01100110, B11111111, B11111111, B11111111,
B11111111, B01111110, B00111100, B00011000
};
byte eyeHeart2[8] = {
B00000000, B00100100, B01111110, B01111110,
B01111110, B00111100, B00011000, B00000000
};
byte eyeHeart3[8] = {
B00000000, B00000000, B00100100, B00111100,
B00111100, B00011000, B00000000, B00000000
};
byte eyeSmall1[8] = {
B01111110, B00000000, B00111100, B01111110,
B01100110, B01100110, B01111110, B00111100
};
byte eyeSmall2[8] = {
B01111110, B00000000, B00000000, B01111110,
B01100110, B01100110, B01111110, B00111100
};
byte eyeSmall3[8] = {
B01111110, B00000000, B00000000, B00000000,
B01100110, B01100110, B01111110, B00111100
};
byte eyeSmall4[8] = {
B01111110, B00000000, B00000000, B00000000,
B00000000, B01100110, B01111110, B00111100
};
byte eyeSmall5[8] = {
B01111110, B00000000, B00000000, B00000000,
B00000000, B00000000, B01111110, B00111100
};
byte eyeSmall6[8] = {
B01111110, B00000000, B00000000, B00000000,
B00000000, B00000000, B00000000, B00111100
};
byte eyeM[8] = {
B10000001, B11000011, B11100111, B11111111,
B11111111, B11011011, B11000011, B11000011
};
byte eyeA[8] = {
B00111100, B01111110, B11000011, B11000011,
B11111111, B11111111, B11000011, B11000011
};
byte eyeR[8] = {
B11111100, B11111110, B11000011, B11000011,
B11111110, B11111100, B11001110, B11000111
};
byte eyeV[8] = {
B11000011, B11000011, B11000011, B11000011,
B11100111, B01111110, B00111100, B00011000
};
byte eyeI[8] = {
B01111110, B01111110, B00011000, B00011000,
B00011000, B00011000, B01111110, B01111110
};
byte eyeN[8] = {
B11000011, B11000011, B11100011, B11110011,
B11011011, B11001111, B11000111, B11000011
};
byte eyeQ[8] = {
B00111100,
B01111110,
B11000011,
B11000011,
B11011011,
B11001111,
B01111110,
B00111101
};
byte eyeU[8] = {
B11000011,
B11000011,
B11000011,
B11000011,
B11000011,
B11000011,
B01111110,
B00111100
};
byte eyeZ[8] = {
B11111111,
B11111111,
B00000111,
B00001110,
B00011100,
B00111000,
B11111111,
B11111111
};
byte eyeG2[8] = {
B00111110,
B01111111,
B11000011,
B11000000,
B11001111,
B11000011,
B01111110,
B00111100
};
byte eyeT[8] = {
B11111111,
B00011000,
B00011000,
B00011000,
B00011000,
B00011000,
B00011000,
B00011000
};
byte eyeO[8] = {
B00111100, B01111110, B11111111, B11100111,
B11100111, B11111111, B01111110, B00111100
};
byte eyeBlank[8] = {
B00000000,B00000000,B00000000,B00000000,
B00000000,B00000000,B00000000,B00000000
};
byte eyeSleep[8] = {
B00000000,
B00111100,
B01111110,
B11111111,
B11111111,
B01111110,
B00111100,
B00000000
};
byte eyeAngry[8] = {
B11111111,
B11111111,
B11100111,
B11000011,
B11000011,
B11111111,
B01111110,
B00111100
};
byte eyeBlock[8] = {
B01111110,
B00000000,
B00111100,
B00111100,
B00111100,
B00111100,
B00000000,
B00000000,
};
byte eye2[8] = {
B00111100,
B01111110,
B11100111,
B00011110,
B00011100,
B00111000,
B11111111,
B11111111
};
// ============================================================================
// 5x7 FONT (columns) for A-Z, space and comma
// ============================================================================
const uint8_t font5x7_A_to_Z[33][5] = { // increased size to 33
{0x7E,0x11,0x11,0x11,0x7E}, // A
{0x7F,0x49,0x49,0x49,0x36}, // B
{0x3E,0x41,0x41,0x41,0x22}, // C
{0x7F,0x41,0x41,0x22,0x1C}, // D
{0x7F,0x49,0x49,0x49,0x41}, // E
{0x7F,0x09,0x09,0x09,0x01}, // F
{0x3E,0x41,0x49,0x49,0x7A}, // G
{0x7F,0x08,0x08,0x08,0x7F}, // H
{0x00,0x41,0x7F,0x41,0x00}, // I
{0x20,0x40,0x41,0x3F,0x01}, // J
{0x7F,0x08,0x14,0x22,0x41}, // K
{0x7F,0x40,0x40,0x40,0x40}, // L
{0x7F,0x02,0x04,0x02,0x7F}, // M
{0x7F,0x04,0x08,0x10,0x7F}, // N
{0x3E,0x41,0x41,0x41,0x3E}, // O
{0x7F,0x09,0x09,0x09,0x06}, // P
{0x3E,0x41,0x51,0x21,0x5E}, // Q
{0x7F,0x09,0x19,0x29,0x46}, // R
{0x46,0x49,0x49,0x49,0x31}, // S
{0x01,0x01,0x7F,0x01,0x01}, // T
{0x3F,0x40,0x40,0x40,0x3F}, // U
{0x1F,0x20,0x40,0x20,0x1F}, // V
{0x3F,0x40,0x38,0x40,0x3F}, // W
{0x63,0x14,0x08,0x14,0x63}, // X
{0x07,0x08,0x70,0x08,0x07}, // Y
{0x61,0x51,0x49,0x45,0x43}, // Z
// Special characters
{0x02,0x01,0x59,0x09,0x06}, // ? -> 26
{0x00,0x00,0x5F,0x00,0x00}, // ! -> 27
{0x14,0x7F,0x14,0x7F,0x14}, // # -> 28
{0x00,0x08,0x08,0x08,0x00}, // - -> 29
{0x36,0x49,0x55,0x22,0x50}, // & -> 30
{0x00,0x60,0x60,0x00,0x00}, // . -> 31
{0x00,0x40,0x60,0x00,0x00} // , -> 32
};
const uint8_t FONT_COMMA[1] = { 0x80 }; // small comma/dot (optional)
// ============================================================================
// FORWARD DECLARATIONS
// ============================================================================
void systemResetAndDisable(); // kept for full reset on demand (not called automatically now)
void checkD13();
void handleExternalPause(); // new
void checkD2D3();
void updateModeButton();
void smartDelay(unsigned long ms, bool allowD2D3, bool breakOnModeChange);
void showBoth(const byte pattern[]);
void showPair(const byte leftPattern[], const byte rightPattern[]);
void autoMode();
void displayIdleAnimation();
void displayBehaviorDefault();
void displayBehaviorHeart();
void displayBehaviorSmall();
void displayBehaviorMarvin();
void displayBehaviorSleepy();
void displayBehaviorAngry();
void displayBehaviorQuizzagan();
void displayBehaviorOff();
void displayBehaviorBlock(); // NEW safe version (outside AUTO)
void displayBehaviorScrollWords(); // NEW
void displaySequenceLeft();
void displaySequenceRight();
void showReactionFace();
void showBoredAnimation();
void showRunningAutoOnce(unsigned long totalMs);
void scrollCueBlink(); // subtle cue before a scroll animation
// Scroll functions
void displayScrollAnim0();
void displayScrollAnim1();
void displayScrollAnim2();
void displayScrollAnim3();
void displayScrollAnim4();
void displayScrollAnim5();
void displayScrollAnim6();
void displayScrollAnim7();
void displayScrollAnim8();
void drawPupilFrame(int xL, int yL, int xR, int yR);
// -----------------------------------------------------------------------------
// INTERRUPT SERVICE ROUTINES
// -----------------------------------------------------------------------------
void handleLeftInterrupt() { leftTriggered = true; }
void handleRightInterrupt() { rightTriggered = true; }
// -----------------------------------------------------------------------------
// BASIC DISPLAY HELPERS
// -----------------------------------------------------------------------------
void showBoth(const byte pattern[]) {
for (int row = 0; row < 8; row++) {
lc1.setRow(0, row, pattern[row]);
lc2.setRow(0, row, pattern[row]);
}
}
void showPair(const byte leftPattern[], const byte rightPattern[]) {
for (int row = 0; row < 8; row++) {
lc1.setRow(0, row, leftPattern[row]);
lc2.setRow(0, row, rightPattern[row]);
}
}
// -----------------------------------------------------------------------------
// SAFETY & SMART DELAY
// -----------------------------------------------------------------------------
void checkD13() {
// New behavior: treat D13 as priority PAUSE (LOW active).
// If D13 goes LOW, we will enter handleExternalPause(), which pauses
// animations and preserves state (scrollIndex, modes).
if (digitalRead(PIN_D13_RESET) == LOW) {
handleExternalPause();
}
}
void handleExternalPause() {
// Pause operations: preserve state, detach interrupts so manual triggers do not fire,
// and show a subtle paused indicator. While paused, AUTO may still be activated;
// if AUTO becomes active we'll return so autoMode() can run. When D13 releases (HIGH),
// resume previous state (especially SCROLL).
pausedByD13 = true;
// Save whether we were in SCROLL mode so we can resume
bool wasScrolling = (currentBehaviorMode == BEHAV_SCROLL);
// Detach manual interrupts — paused state prevents manual actions
detachInterrupt(digitalPinToInterrupt(PIN_D2_LEFT));
detachInterrupt(digitalPinToInterrupt(PIN_D3_RIGHT));
// Use D1 as an indicator during pause: we'll set it HIGH (not SCROLL)
digitalWrite(PIN_D1_SIGNAL, HIGH);
// Show a simple paused blink pattern until D13 returns HIGH or AUTO asserts
while (digitalRead(PIN_D13_RESET) == LOW) {
// CLEAR DISPLAYS
lc1.clearDisplay(0);
lc2.clearDisplay(0);
delay(80);
// keep checking D13 and AUTO
}
// D13 returned HIGH — resume.
pausedByD13 = false;
// If we were scrolling, ensure D1 returns to LOW, re-attach interrupts and resume scroll in caller.
if (wasScrolling) {
digitalWrite(PIN_D1_SIGNAL, LOW);
// Re-attach interrupts so displayBehaviorScrollWords can detach again when re-entered.
attachInterrupt(digitalPinToInterrupt(PIN_D2_LEFT), handleLeftInterrupt, FALLING);
attachInterrupt(digitalPinToInterrupt(PIN_D3_RIGHT), handleRightInterrupt, FALLING);
// We'll return to the caller; caller (loop) will re-enter displayBehaviorScrollWords() if needed.
} else {
// Not scrolling: restore interrupts and return to normal flow.
attachInterrupt(digitalPinToInterrupt(PIN_D2_LEFT), handleLeftInterrupt, FALLING);
attachInterrupt(digitalPinToInterrupt(PIN_D3_RIGHT), handleRightInterrupt, FALLING);
lastActivityMs = millis();
}
}
void smartDelay(unsigned long ms, bool allowD2D3, bool breakOnModeChange) {
unsigned long start = millis();
while (millis() - start < ms) {
delay(1);
checkD13(); // this may cause a pause and return early from animation
// If D13 paused and auto asserted, caller will see isAuto() true and handle it.
updateModeButton();
if (allowD2D3) {
checkD2D3();
}
if (breakOnModeChange && modeChanged) return;
// Also break if pausedByD13 so caller can detect pause quickly
if (pausedByD13) return;
}
}
// -----------------------------------------------------------------------------
// HELPER: isAuto()
// -----------------------------------------------------------------------------
bool isAuto() {
return digitalRead(PIN_D4_AUTO) == LOW;
}
// -----------------------------------------------------------------------------
// BUTTON HANDLERS (A0 disabled while AUTO active)
// Short press: cycle modes (normal). In SCROLL mode, short press advances scroll.
// Long press (>=3s): enter/exit SCROLL mode.
// -----------------------------------------------------------------------------
void updateModeButton() {
if (isAuto()) return; // A0 disabled while AUTO active
if (pausedByD13) return; // buttons ignored while D13 pause active
bool reading = digitalRead(PIN_A0_MODE);
unsigned long now = millis();
// Detect press start
if (lastA0State == HIGH && reading == LOW) {
a0LowStartTime = now;
}
if (reading == LOW) {
// LONG PRESS handling
if (!a0LongLatched && (now - a0LowStartTime) >= A0_LONG_MS) {
a0LongLatched = true;
a0PressLatched = true; // prevent short press handling
modeChanged = true;
if (currentBehaviorMode != BEHAV_SCROLL) {
// ===== ENTER SCROLL MODE =====
previousBehaviorMode = currentBehaviorMode;
scrollIndex = 0; // start from first scroll
exitScrollMode = false;
// Visual cue: M -> Q sequence
lc1.clearDisplay(0);
lc2.clearDisplay(0);
delay(40);
for (int i = 0; i < 1; i++) {
showBoth(eyeM);
smartDelay(350, false, false);
showBoth(eyeQ);
smartDelay(350, false, false);
showBoth(eyeBlank);
smartDelay(80, false, false);
}
currentBehaviorMode = BEHAV_SCROLL;
digitalWrite(PIN_D1_SIGNAL, LOW);
displayBehaviorScrollWords();
modeChanged = false;
} else {
// ===== EXIT SCROLL MODE =====
exitScrollMode = true;
currentBehaviorMode = previousBehaviorMode;
// Visual cue: M -> Q sequence on exit
lc1.clearDisplay(0);
lc2.clearDisplay(0);
delay(40);
for (int i = 0; i < 1; i++) {
showBoth(eyeM);
smartDelay(350, false, false);
showBoth(eyeQ);
smartDelay(350, false, false);
showBoth(eyeBlank);
smartDelay(80, false, false);
}
digitalWrite(PIN_D1_SIGNAL, HIGH); // return indicator HIGH
}
}
// SHORT PRESS handling (debounced)
else if (!a0PressLatched && (now - a0LowStartTime) >= A0_DEBOUNCE_MS && !a0LongLatched) {
a0PressLatched = true;
if (currentBehaviorMode == BEHAV_SCROLL) {
// Advance to next scroll animation (wrap)
scrollIndex = (scrollIndex + 1) % NUM_SCROLLS;
// set modeChanged to interrupt current smartDelay loops and immediately switch
modeChanged = true;
lastActivityMs = now;
} else {
// Normal mode cycling (ONLY the original behaviors 0..8)
currentBehaviorMode = (BehaviorMode)((currentBehaviorMode + 1) % 9); // cycles 0..8
modeChanged = true;
lastActivityMs = now;
// ensure D1 stays HIGH in normal modes
digitalWrite(PIN_D1_SIGNAL, HIGH);
}
}
} else {
// Button released -> clear latches so next press works
a0PressLatched = false;
a0LongLatched = false;
}
lastA0State = reading;
}
// -----------------------------------------------------------------------------
// RESET HANDLING (kept but not auto-called by D13 anymore)
// -----------------------------------------------------------------------------
void systemResetAndDisable() {
// Full reset routine (kept for manual call if you want destructive reset)
lc1.clearDisplay(0);
lc2.clearDisplay(0);
detachInterrupt(digitalPinToInterrupt(PIN_D2_LEFT));
detachInterrupt(digitalPinToInterrupt(PIN_D3_RIGHT));
setup();
while (digitalRead(PIN_D13_RESET) == LOW) { }
}
// -----------------------------------------------------------------------------
// D2 / D3 POLLING CHECK
// -----------------------------------------------------------------------------
void checkD2D3() {
if (isAuto()) return;
if (pausedByD13) return;
if (digitalRead(PIN_D2_LEFT) == LOW) displaySequenceRight();
if (digitalRead(PIN_D3_RIGHT) == LOW) displaySequenceLeft();
}
// -----------------------------------------------------------------------------
// SETUP
// -----------------------------------------------------------------------------
void setup() {
pinMode(PIN_D4_AUTO, INPUT_PULLUP);
pinMode(PIN_D2_LEFT, INPUT_PULLUP);
pinMode(PIN_D3_RIGHT, INPUT_PULLUP);
pinMode(PIN_A0_MODE, INPUT_PULLUP);
pinMode(PIN_D13_RESET, INPUT);
// Setup D1 signal pin as OUTPUT
pinMode(PIN_D1_SIGNAL, OUTPUT);
lc1.shutdown(0, false);
lc1.setIntensity(0, 8);
lc1.clearDisplay(0);
lc2.shutdown(0, false);
lc2.setIntensity(0, 8);
lc2.clearDisplay(0);
// default: normal mode -> D1 HIGH
digitalWrite(PIN_D1_SIGNAL, HIGH);
randomSeed(analogRead(A1));
attachInterrupt(digitalPinToInterrupt(PIN_D2_LEFT), handleLeftInterrupt, FALLING);
attachInterrupt(digitalPinToInterrupt(PIN_D3_RIGHT), handleRightInterrupt, FALLING);
lastActivityMs = millis();
}
// -----------------------------------------------------------------------------
// SCROLL WORDS MODE (master loop) - interruptible and priority-aware
// -----------------------------------------------------------------------------
void displayBehaviorScrollWords() {
modeChanged = false;
exitScrollMode = false;
// Prevent manual interrupts firing while in SCROLL mode
detachInterrupt(digitalPinToInterrupt(PIN_D2_LEFT));
detachInterrupt(digitalPinToInterrupt(PIN_D3_RIGHT));
// Ensure indicator pin is LOW for SCROLL mode
digitalWrite(PIN_D1_SIGNAL, LOW);
bool interruptedByAuto = false;
bool interruptedByD13 = false;
while (!exitScrollMode) {
// if D13 goes LOW, handle external pause and break so caller can resume later
if (digitalRead(PIN_D13_RESET) == LOW) {
interruptedByD13 = true;
pausedByD13 = true;
// call handleExternalPause() -> it will wait until D13 HIGH or AUTO active
handleExternalPause();
// if AUTO became active during pause, let loop()/autoMode handle AUTO.
if (isAuto()) {
// leave scroll preserved so loop() will re-enter scroll later
interruptedByAuto = true;
break;
} else {
// D13 released: re-enter scroll loop (continue)
pausedByD13 = false;
digitalWrite(PIN_D1_SIGNAL, LOW);
}
}
// If AUTO becomes active while scrolling, break immediately so AUTO takes over.
if (isAuto()) {
interruptedByAuto = true;
break;
}
modeChanged = false;
// Call the selected scroll animation ONCE (it should return quickly).
switch (scrollIndex) {
case 0: displayScrollAnim0(); break;
case 1: displayScrollAnim1(); break;
case 2: displayScrollAnim2(); break;
case 3: displayScrollAnim3(); break;
case 4: displayScrollAnim4(); break;
case 5: displayScrollAnim5(); break;
case 6: displayScrollAnim6(); break;
case 7: displayScrollAnim7(); break;
case 8: displayScrollAnim8(); break;
default: displayScrollAnim0(); break;
}
if (modeChanged) {
if (exitScrollMode) break;
modeChanged = false;
continue;
}
// repeat the same scroll animation again unless changed
smartDelay(80, false, true);
if (exitScrollMode) break;
}
// If we exited normally (not due to immediate AUTO), re-enable interrupts and restore previous mode/state
if (!interruptedByAuto && !interruptedByD13) {
attachInterrupt(digitalPinToInterrupt(PIN_D2_LEFT), handleLeftInterrupt, FALLING);
attachInterrupt(digitalPinToInterrupt(PIN_D3_RIGHT), handleRightInterrupt, FALLING);
// Ensure indicator pin reflects that we're back in normal mode
digitalWrite(PIN_D1_SIGNAL, HIGH);
// restore previous behavior mode (if updateModeButton didn't already)
currentBehaviorMode = previousBehaviorMode;
modeChanged = false;
exitScrollMode = false;
lastActivityMs = millis();
} else {
// If interruptedByAuto: preserve scrollIndex/currentBehaviorMode==BEHAV_SCROLL/previousBehaviorMode
// If interruptedByD13: handleExternalPause() handled waiting; if D13 released without AUTO, we re-entered loop.
// Do not change scrollIndex or previousBehaviorMode here; loop() will re-enter scroll if needed.
}
}
// -----------------------------------------------------------------------------
// SCROLLER HELPERS (MODIFIED: bigger gap between words) & scrollText is aware
// -----------------------------------------------------------------------------
void buildColumnBufferForText(const char* txt, uint8_t *cols, int &colCount) {
int idx = 0;
// prepend 16 blank columns
for (int b = 0; b < 16; b++) {
cols[idx++] = 0x00;
}
for (const char* p = txt; *p != '\0'; ++p) {
char c = *p;
// convert lowercase to uppercase
if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
if (c >= 'A' && c <= 'Z') {
int fi = c - 'A';
for (int k = 0; k < 5; k++) cols[idx++] = font5x7_A_to_Z[fi][k];
cols[idx++] = 0x00; // single-column spacer between letters
} else if (c == ' ') {
// bigger gap between words: insert 6 blank columns
for (int s = 0; s < 6; s++) cols[idx++] = 0x00;
} else {
// handle special characters using switch
int fi = -1; // font index
switch(c) {
case '.': fi = 31; break;
case ',': fi = 32; break;
case '?': fi = 26; break;
case '!': fi = 27; break;
case '#': fi = 28; break;
case '&': fi = 30; break;
case '-': fi = 29; break; // dash
default: break;
}
if (fi >= 0) {
for (int k = 0; k < 5; k++) cols[idx++] = font5x7_A_to_Z[fi][k];
cols[idx++] = 0x00; // single-column spacer
} else {
// unknown character -> treat as space with big gap
for (int s = 0; s < 6; s++) cols[idx++] = 0x00;
}
}
// Safety: avoid buffer overflow
if (idx > 1024 - 10) break;
}
colCount = idx; // return actual column count
}
void renderWindowFromColumns(const uint8_t *cols, int colCount, int offset) {
byte leftBmp[8];
byte rightBmp[8];
for (int r = 0; r < 8; r++) {
leftBmp[r] = 0x00;
rightBmp[r] = 0x00;
}
for (int c = 0; c < 16; c++) {
int colIdx = offset + c;
uint8_t colData = 0x00;
if (colIdx >= 0 && colIdx < colCount) colData = cols[colIdx];
for (int row = 0; row < 7; row++) { // font has 7 rows
bool pixelOn = (colData & (1 << row)) != 0;
if (pixelOn) {
int targetRow = row + 1; // SHIFT DOWN by 1
if (targetRow >= 8) continue; // safety, do not exceed row 7
if (c < 8) leftBmp[targetRow] |= (1 << (7 - c));
else rightBmp[targetRow] |= (1 << (7 - (c - 8)));
}
}
// row 0 stays 0 (top blank)
}
for (int r = 0; r < 8; r++) {
lc1.setRow(0, r, leftBmp[r]);
lc2.setRow(0, r, rightBmp[r]);
}
}
// Smoothly scroll the given text across both matrices.
// speedMs controls delay per column shift.
// This function now checks isAuto() and D13 frequently and will return immediately
// if either becomes active so priority handlers can take over.
void scrollText(const char* txt, unsigned long speedMs) {
static uint8_t cols[1024];
int colCount = 0;
buildColumnBufferForText(txt, cols, colCount);
int maxOffset = colCount - 16;
if (maxOffset < 0) maxOffset = 0;
for (int offset = 0; offset <= maxOffset; offset++) {
// If D13 pause asserted, abort scroll immediately (preserve scroll state)
if (digitalRead(PIN_D13_RESET) == LOW) {
pausedByD13 = true;
handleExternalPause();
// If AUTO became active during pause, return to caller so loop() triggers autoMode()
if (isAuto()) {
return;
}
// otherwise continue scrolling after pause (preserve offset/scrollIndex)
}
// If AUTO becomes active, abort the scroll immediately so AUTO can take over.
if (isAuto()) {
return;
}
if (modeChanged || exitScrollMode) return; // interruptible
renderWindowFromColumns(cols, colCount, offset);
smartDelay(speedMs, false, true);
if (pausedByD13) return;
}
if (!modeChanged && !exitScrollMode) {
smartDelay(200, false, true);
lc1.clearDisplay(0);
lc2.clearDisplay(0);
smartDelay(80, false, true);
}
}
// -----------------------------------------------------------------------------
// Scroll animation implementations (phrases you provided)
// -----------------------------------------------------------------------------
void displayScrollAnim0() { scrollText("HI THERE! MY NAME IS E-MAR.", 110); }
void displayScrollAnim1() { scrollText("GOOD MORNING. I HOPE YOU ARE DOING WELL TODAY", 110); }
void displayScrollAnim2() { scrollText("GOOD AFTERNOON! WISHING YOU A PLEASANT REST OF THE DAY", 110); }
void displayScrollAnim3() { scrollText("YOUR PERSONALITY IS AMAZING, KEEP SHINING", 110); }
void displayScrollAnim4() { scrollText("THANK YOU! HAVE A WONDERFUL DAY", 110); }
void displayScrollAnim5() { scrollText("HOW MAY I ASSIST YOU?", 110); }
void displayScrollAnim6() { scrollText("WOULD YOU MIND TO TALK WITH MY CREATOR", 110); }
void displayScrollAnim7() { scrollText("GREAT CHAT! I HAVE TO HEAD OFF", 110); }
void displayScrollAnim8() { scrollText("GOD BLESS US ALL.", 110); }
// -----------------------------------------------------------------------------
// RUNNING "AUTO" MARQUEE – SMOOTH VERSION
// -----------------------------------------------------------------------------
void showRunningAutoOnce(unsigned long totalMs) {
const byte* letters[] = { eyeA, eyeU, eyeT, eyeO };
const int numLetters = 4;
const int stepsPerLetter = 2;
unsigned long frameMs = totalMs / (numLetters * stepsPerLetter);
if (frameMs < 50) frameMs = 50;
for (int i = 0; i < numLetters; i++) {
const byte* leftLetter = letters[i];
const byte* rightLetter = (i < numLetters-1) ? letters[i+1] : eyeBlank;
showPair(leftLetter, rightLetter);
smartDelay(frameMs, false, true);
showPair(eyeBlank, rightLetter);
smartDelay(frameMs, false, true);
}
showPair(eyeBlank, eyeBlank);
smartDelay(50, false, true);
}
// ------------------------- REPLACEMENT autoMode() ---------------------------
//
// Ensures the "AUTO" running marquee plays immediately and repeatedly,
// then runs the current behavior's animation in short frames while AUTO is active.
// AUTO overrides all other inputs; previous mode is resumed after AUTO ends.
// ----------------------------------------------------------------------------
void autoMode() {
// Save and clear manual triggers
leftTriggered = false;
rightTriggered = false;
// Prevent manual interrupts while in AUTO
detachInterrupt(digitalPinToInterrupt(PIN_D2_LEFT));
detachInterrupt(digitalPinToInterrupt(PIN_D3_RIGHT));
// When AUTO is active we want the normal indicator to be HIGH so it doesn't
// conflict with SCROLL's LOW indicator. (If we resume SCROLL after AUTO,
// loop() will set D1 LOW and re-enter scroll.)
digitalWrite(PIN_D1_SIGNAL, HIGH);
// Always show the running "AUTO" marquee immediately when entering AUTO
showRunningAutoOnce(autoRunMs);
unsigned long lastAutoIndicatorMs = millis();
// MAIN AUTO LOOP - continues until isAuto() becomes false
while (isAuto()) {
// Check reset/pause each cycle
checkD13();
// Play the running AUTO marquee again periodically
if (millis() - lastAutoIndicatorMs >= autoIntervalMs) {
showRunningAutoOnce(autoRunMs);
lastAutoIndicatorMs = millis();
}
// Run a short non-blocking frame for the current behavior
switch (currentBehaviorMode) {
case BEHAV_SLEEPY:
displayBehaviorSleepy();
break;
case BEHAV_ANGRY:
displayBehaviorAngry();
break;
case BEHAV_MARVIN:
displayBehaviorMarvin();
break;
case BEHAV_QUIZZAGAN:
displayBehaviorQuizzagan();
break;
case BEHAV_HEART:
displayBehaviorHeart();
break;
case BEHAV_SMALL:
displayBehaviorSmall();
break;
case BEHAV_BLOCK:
// short, non-blocking block frame while AUTO is active.
if (random(0,10) < 8) {
showBoth(eyeBlock);
smartDelay(150, false, true); // short hold
} else {
showBoth(eyeBlank);
smartDelay(150, false, true); // short hold
}
break;
case BEHAV_DEFAULT:
default:
// Short default-frame while AUTO is active
showBoth(eyeOpen); smartDelay(200, false, true);
if (random(0, 6) == 0) {
showBoth(eyeWink); smartDelay(80, false, true);
showBoth(eyeClosedTotal); smartDelay(80, false, true);
}
break;
}
// Clear any modeChanged flag set inside called routines
if (modeChanged) modeChanged = false;
// keep checking reset / AUTO status at a fine granularity
checkD13();
}
// Re-enable manual interrupts when AUTO exits
attachInterrupt(digitalPinToInterrupt(PIN_D2_LEFT), handleLeftInterrupt, FALLING);
attachInterrupt(digitalPinToInterrupt(PIN_D3_RIGHT), handleRightInterrupt, FALLING);
// Reset activity timer
lastActivityMs = millis();
}
// LEFT / RIGHT MANUAL SEQUENCES
void displaySequenceLeft() {
lastActivityMs = millis();
while (digitalRead(PIN_D3_RIGHT) == LOW) {
if (isAuto()) {
autoMode();
return;
}
if (pausedByD13) return;
showBoth(eyeOpen); smartDelay(80, false, false);
showBoth(eyeLeft1); smartDelay(80, false, false);
showBoth(eyeLeft2); smartDelay(80, false, false);
showBoth(eyeLeft3); smartDelay(120, false, false);
showBoth(eyeLeft2); smartDelay(80, false, false);
showBoth(eyeLeft1); smartDelay(80, false, false);
showBoth(eyeOpen); smartDelay(80, false, false);
showBoth(eyeWink); smartDelay(80, false, false);
showBoth(eyeClosed); smartDelay(80, false, false);
showBoth(eyeClosedTotal); smartDelay(80, false, false);
showBoth(eyeClosed); smartDelay(80, false, false);
showBoth(eyeWink); smartDelay(80, false, false);
showReactionFace();
if (digitalRead(PIN_D3_RIGHT) == HIGH) {
displayIdleAnimation();
return;
}
}
}
void displaySequenceRight() {
lastActivityMs = millis();
while (digitalRead(PIN_D2_LEFT) == LOW) {
if (isAuto()) {
autoMode();
return;
}
if (pausedByD13) return;
showBoth(eyeOpen); smartDelay(80, false, false);
showBoth(eyeRight1); smartDelay(80, false, false);
showBoth(eyeRight2); smartDelay(80, false, false);
showBoth(eyeRight3); smartDelay(120, false, false);
showBoth(eyeRight2); smartDelay(80, false, false);
showBoth(eyeRight1); smartDelay(80, false, false);
showBoth(eyeOpen); smartDelay(80, false, false);
showBoth(eyeWink); smartDelay(80, false, false);
showBoth(eyeClosed); smartDelay(80, false, false);
showBoth(eyeClosedTotal); smartDelay(80, false, false);
showBoth(eyeClosed); smartDelay(80, false, false);
showBoth(eyeWink); smartDelay(80, false, false);
showReactionFace();
if (digitalRead(PIN_D2_LEFT) == HIGH) {
displayIdleAnimation();
return;
}
}
}
// BEHAVIOR MODES (short-press cycle covers only 0..8)
void displayBehaviorDefault() {
modeChanged = false;
for (int i = 0; i < 30; i++) {
showBoth(eyeOpen);
smartDelay(60, true, true);
if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
}
showBoth(eyeWink); smartDelay(40, true, true); if (modeChanged) return;
showBoth(eyeClosed); smartDelay(40, true, true); if (modeChanged) return;
showBoth(eyeClosedTotal); smartDelay(40, true, true); if (modeChanged) return;
showBoth(eyeOpen); smartDelay(80, true, true); if (modeChanged) return;
}
void displayBehaviorHeart() {
modeChanged = false;
showPair(eyeHeart1, eyeHeart3); smartDelay(120, true, true); if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
showPair(eyeHeart2, eyeHeart2); smartDelay(120, true, true); if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
showPair(eyeHeart3, eyeHeart1); smartDelay(120, true, true); if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
showPair(eyeBlank, eyeBlank); smartDelay(120, true, true); if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
showPair(eyeHeart3, eyeHeart1); smartDelay(120, true, true); if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
showPair(eyeHeart2, eyeHeart2); smartDelay(120, true, true); if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
showPair(eyeHeart1, eyeHeart3); smartDelay(120, true, true); if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
showPair(eyeBlank, eyeBlank); smartDelay(120, true, true); if (modeChanged) return;
}
void displayBehaviorSmall() {
modeChanged = false;
showBoth(eyeSmall1); smartDelay(600, true, true); if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
showBoth(eyeSmall2); smartDelay(60, true, true); if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
showBoth(eyeSmall3); smartDelay(60, true, true); if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
showBoth(eyeSmall4); smartDelay(60, true, true); if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
showBoth(eyeSmall5); smartDelay(60, true, true); if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
showBoth(eyeSmall6); smartDelay(120, true, true); if (modeChanged) return;
}
void displayBehaviorMarvin() {
modeChanged = false;
showPair(eyeBlank, eyeM); smartDelay(marvinStepMs, true, true); if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
showPair(eyeM, eyeA); smartDelay(marvinStepMs, true, true); if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
showPair(eyeA, eyeR); smartDelay(marvinStepMs, true, true); if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
showPair(eyeR, eyeV); smartDelay(marvinStepMs, true, true); if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
showPair(eyeV, eyeI); smartDelay(marvinStepMs, true, true); if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
showPair(eyeI, eyeN); smartDelay(marvinStepMs, true, true); if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
showPair(eyeN, eyeBlank); smartDelay(marvinStepMs, true, true); if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
showPair(eyeBlank, eyeBlank); smartDelay(marvinStepMs, true, true); if (modeChanged) return;
smartDelay(marvinPauseMs, true, true);
}
void displayBehaviorSleepy() {
modeChanged = false;
showBoth(eyeOpen); smartDelay(300, true, true); if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
showBoth(eyeSleep); smartDelay(400, true, true); if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
showBoth(eyeClosed); smartDelay(400, true, true); if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
showBoth(eyeSleep); smartDelay(500, true, true); if (modeChanged) return;
}
void displayBehaviorAngry() {
modeChanged = false;
const float CXF = 4.0f;
const float CYF = 3.0f;
const float MIN_RADIUS = 0.0f;
const float MAX_RADIUS = 3.0f;
const int STEPS_PER_REV = 12;
const int REVOLUTIONS = 3;
const int CYCLES = 3;
const unsigned long FRAME_MS = 70;
const int FRAMES_PER_CYCLE = STEPS_PER_REV * REVOLUTIONS;
for (int c = 0; c < CYCLES; c++) {
for (int f = 0; f < FRAMES_PER_CYCLE; f++) {
checkD13();
if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
float t = (float)f / (float)FRAMES_PER_CYCLE;
float rFactor = 0.5f * (1.0f - cos(2.0f * PI * t));
float radius = MIN_RADIUS + (MAX_RADIUS - MIN_RADIUS) * rFactor;
float angle = 2.0f * PI * (REVOLUTIONS * t);
float xL_f = CXF + radius * cos(angle);
float yL_f = CYF + radius * sin(angle);
float xR_f = CXF + radius * cos(-angle);
float yR_f = CYF + radius * sin(-angle);
int xL = (int)round(xL_f);
int yL = (int)round(yL_f);
int xR = (int)round(xR_f);
int yR = (int)round(yR_f);
xL = constrain(xL, 0, 7);
yL = constrain(yL, 0, 7);
xR = constrain(xR, 0, 7);
yR = constrain(yR, 0, 7);
drawPupilFrame(xL, yL, xR, yR);
smartDelay(FRAME_MS, true, true);
checkD13();
if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
}
}
showBoth(eyeSmall1);
smartDelay(220, true, true);
if (modeChanged) return;
}
void displayBehaviorQuizzagan() {
modeChanged = false;
const byte* wordLeft[11] = { eyeBlank, eyeQ, eyeU, eyeI, eyeZ, eyeZ, eyeA, eyeG2, eyeA, eyeN, eyeBlank };
const byte* wordRight[11] = { eyeQ, eyeU, eyeI, eyeZ, eyeZ, eyeA, eyeG2, eyeA, eyeN, eyeBlank, eyeBlank };
for (int i = 0; i < 10; i++) {
showPair(wordLeft[i], wordRight[i]);
smartDelay(quizzaganStepMs, true, true);
if (modeChanged) return;
if (isAuto()) return;
if (pausedByD13) return;
}
showPair(eyeBlank,eyeBlank);
smartDelay(quizzaganPauseMs, true, true);
}
void displayBehaviorOff() {
modeChanged = false;
showBoth(eyeBlank);
lc1.clearDisplay(0);
lc2.clearDisplay(0);
delay(20);
lc1.setIntensity(0, 0);
lc2.setIntensity(0, 0);
delay(5);
lc1.shutdown(0, true);
lc2.shutdown(0, true);
detachInterrupt(digitalPinToInterrupt(PIN_D2_LEFT));
detachInterrupt(digitalPinToInterrupt(PIN_D3_RIGHT));
while (!modeChanged) {
checkD13();
updateModeButton();
smartDelay(100, false, true);
}
lc1.shutdown(0, false);
lc2.shutdown(0, false);
lc1.setIntensity(0, 8);
lc2.setIntensity(0, 8);
lc1.clearDisplay(0);
lc2.clearDisplay(0);
attachInterrupt(digitalPinToInterrupt(PIN_D2_LEFT), handleLeftInterrupt, FALLING);
attachInterrupt(digitalPinToInterrupt(PIN_D3_RIGHT), handleRightInterrupt, FALLING);
modeChanged = false;
}
// -----------------------------------------------------------------------------
// SAFE displayBehaviorBlock() used outside AUTO (will return immediately on AUTO or D13 pause)
// -----------------------------------------------------------------------------
void displayBehaviorBlock() {
modeChanged = false;
while (!modeChanged) {
// If AUTO is asserted, exit immediately so autoMode() can run
if (isAuto()) return;
if (random(0, 10) < 8) {
showBoth(eyeBlock);
// allow early exit if modeChanged/reset/AUTO occurs
smartDelay(400 + random(0, 200), false, true);
} else {
showBoth(eyeBlank);
smartDelay(100 + random(0, 150), false, true);
}
// --- check for LEFT/RIGHT triggers while block is running ---
checkD2D3(); // allowD2D3 = true
if (leftTriggered) {
leftTriggered = false;
displaySequenceLeft();
}
if (rightTriggered) {
rightTriggered = false;
displaySequenceRight();
}
smartDelay(300, true, true); // allowD2D3 = true
// double-check AUTO after the update
if (isAuto()) return;
}
}
// -----------------------------------------------------------------------------
// REACTIONS / BORED / IDLE
// -----------------------------------------------------------------------------
void showReactionFace() {
switch (currentBehaviorMode) {
case BEHAV_SLEEPY:
showBoth(eyeSleep);
smartDelay(180, false, false);
break;
case BEHAV_ANGRY:
showBoth(eyeAngry);
smartDelay(180, false, false);
break;
case BEHAV_QUIZZAGAN:
showPair(eyeQ, eyeU);
smartDelay(220, false, false);
break;
case BEHAV_HEART:
showPair(eyeHeart1, eyeHeart1);
smartDelay(200, false, false);
break;
case BEHAV_SMALL:
showBoth(eyeSmall3);
smartDelay(200, false, false);
break;
case BEHAV_MARVIN:
showPair(eyeM, eyeN);
smartDelay(220, false, false);
break;
case BEHAV_OFF:
showBoth(eyeBlank);
smartDelay(180, false, false);
break;
case BEHAV_BLOCK:
showBoth(eyeBlock);
smartDelay(180, false, false);
break;
case BEHAV_DEFAULT:
default:
showBoth(eyeOpen);
smartDelay(180, false, false);
break;
}
}
void showBoredAnimation() {
showBoth(eyeLeft2); smartDelay(250, true, true);
showBoth(eyeRight2); smartDelay(250, true, true);
showBoth(eyeOpen); smartDelay(400, true, true);
showBoth(eyeClosed); smartDelay(300, true, true);
showBoth(eyeSleep); smartDelay(400, true, true);
lastActivityMs = millis();
}
void displayIdleAnimation() {
modeChanged = false;
if (millis() - lastActivityMs > BORED_MS) {
showBoredAnimation();
return;
}
switch (currentBehaviorMode) {
case BEHAV_HEART:
displayBehaviorHeart();
break;
case BEHAV_SMALL:
displayBehaviorSmall();
break;
case BEHAV_MARVIN:
displayBehaviorMarvin();
break;
case BEHAV_SLEEPY:
displayBehaviorSleepy();
break;
case BEHAV_ANGRY:
displayBehaviorAngry();
break;
case BEHAV_QUIZZAGAN:
displayBehaviorQuizzagan();
break;
case BEHAV_OFF:
displayBehaviorOff();
break;
case BEHAV_BLOCK:
displayBehaviorBlock();
break;
case BEHAV_DEFAULT:
default:
displayBehaviorDefault();
break;
}
}
// -----------------------------------------------------------------------------
// MAIN LOOP - AUTO and D13 priority (check AUTO and D13 first).
// -----------------------------------------------------------------------------
void loop() {
// Periodic check for D13 priority pause
checkD13();
// If AUTO pin active, immediately run autoMode (priority)
if (isAuto()) {
// Save current mode and whether we are in SCROLL so we can resume it after AUTO
BehaviorMode savedMode = currentBehaviorMode;
bool resumeScrollAfterAuto = (savedMode == BEHAV_SCROLL);
// If SCROLL was active we keep scrollIndex and previousBehaviorMode intact;
// autoMode() will run until isAuto() false.
autoMode(); // returns when isAuto() false
// After AUTO deactivates: resume either SCROLL or saved mode
if (resumeScrollAfterAuto && !pausedByD13) {
// Ensure indicator pin shows SCROLL and continue SCROLL where left off
currentBehaviorMode = BEHAV_SCROLL;
digitalWrite(PIN_D1_SIGNAL, LOW);
// Re-enter scroll loop - this will detach interrupts again and resume scrolling
displayBehaviorScrollWords();
// displayBehaviorScrollWords returns only when scroll mode requested exit or interrupted again
} else {
// restore previous non-scroll mode
currentBehaviorMode = savedMode;
modeChanged = false;
lastActivityMs = millis();
}
// Return early to avoid processing other inputs in same loop cycle
return;
}
// If D13 pause asserted, handle it (handleExternalPause will wait until D13 HIGH or AUTO)
if (digitalRead(PIN_D13_RESET) == LOW) {
handleExternalPause();
// If AUTO asserted during pause, next loop will go into autoMode()
if (isAuto()) return;
// If we resume and were in SCROLL, re-enter scroll
if (currentBehaviorMode == BEHAV_SCROLL && !pausedByD13) {
digitalWrite(PIN_D1_SIGNAL, LOW);
displayBehaviorScrollWords();
return;
}
}
// Only update mode/button when not in AUTO or paused
updateModeButton();
// Normal operation: check triggers then idle behavior
if (leftTriggered) {
leftTriggered = false;
displaySequenceRight();
} else if (rightTriggered) {
rightTriggered = false;
displaySequenceLeft();
} else if (digitalRead(PIN_D2_LEFT) == LOW) {
displaySequenceRight();
} else if (digitalRead(PIN_D3_RIGHT) == LOW) {
displaySequenceLeft();
} else {
displayIdleAnimation();
}
}
// Draw 3x3 pupils helper
void drawPupilFrame(int xL, int yL, int xR, int yR) {
byte leftBmp[8];
byte rightBmp[8];
for (int i = 0; i < 8; i++) { leftBmp[i] = 0x00; rightBmp[i] = 0x00; }
xL = constrain(xL, 0, 7); yL = constrain(yL, 0, 7);
xR = constrain(xR, 0, 7); yR = constrain(yR, 0, 7);
for (int dy = -1; dy <= 1; dy++) {
int yy = yL + dy;
if (yy < 0 || yy > 7) continue;
for (int dx = -1; dx <= 1; dx++) {
int xx = xL + dx;
if (xx < 0 || xx > 7) continue;
leftBmp[yy] |= (1 << (7 - xx));
}
}
for (int dy = -1; dy <= 1; dy++) {
int yy = yR + dy;
if (yy < 0 || yy > 7) continue;
for (int dx = -1; dx <= 1; dx++) {
int xx = xR + dx;
if (xx < 0 || xx > 7) continue;
rightBmp[yy] |= (1 << (7 - xx));
}
}
showPair(leftBmp, rightBmp);
}
// -----------------------------------------------------------------------------
// SCROLL CUE BLINK - subtle one-time cue before a new scroll animation starts
// -----------------------------------------------------------------------------
void scrollCueBlink() {
// short blank -> quick flash -> blank (subtle)
lc1.clearDisplay(0);
lc2.clearDisplay(0);
delay(80);
showBoth(eyeBlock);
delay(120);
lc1.clearDisplay(0);
lc2.clearDisplay(0);
delay(60);
lastActivityMs = millis();
}