// ============================================================================
// EMAR EYES PERSONALITY FRAMEWORK (PWM REMOVED) - Arduino Nano B - 100% WORKING W/ D8 OUTPUT
// DESIGNED AND CREATED BY: MARVIN A. QUIZZAGAN, FEBRUARY 21, 2026
// FIXED / UPDATED (THIS VERSION): 100% WORKING - PERFECT
// 1) D1 REMOVED. ALL INDICATOR FUNCTIONS MOVED TO D8.
// 2) D8 INDICATOR LOGIC SELECTABLE: ACTIVE-LOW or ACTIVE-HIGH.
// 3) AUTO MODE FREEZE FIX:
// - D13 (PAUSE) is INPUT_PULLUP so it never floats.
// 4) AUTO MODE NOW USES THE SAME ANIMATIONS AS MANUAL MODE.
// 5) SRAM FIX (Nano/Uno 2KB RAM):
// - REMOVED 1024-BYTE scroll buffer.
// - FONT + LONG MESSAGES MOVED TO PROGMEM.
// - SCROLLING IS STREAMED with only a 16-byte window buffer.
//
// ✅ NEW UPDATE (FEB 21, 2026):
// 6) ADDED A1 MODE INPUT (INPUT_PULLUP), exact mirror of A0.
// - A1 HAS PRIORITY over A0 when LOW (pressed).
// - ✅ PRIORITY LOCK: While A1 is LOW, A0 is HARD-IGNORED (no queued press).
//
// AUTO: D4 LOW (INPUT_PULLUP)
// PAUSE: D13 LOW (INPUT_PULLUP)
// ============================================================================
#include <LedControl.h>
#include <Arduino.h>
#include <avr/pgmspace.h>
// ✅ FIX: forward declare so Arduino/Wokwi auto-prototypes compile
struct ModeBtnState;
// ------------------------ 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
unsigned long autoFrameGapMs = 60; // breathing gap each AUTO loop
// ------------------------ 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 -------------------------
// D8 indicator output pin.
// true = ACTIVE-LOW (SCROLL active -> LOW, normal -> HIGH)
// false = ACTIVE-HIGH (SCROLL active -> HIGH, normal -> LOW)
const bool INDICATOR_ACTIVE_LOW = false;
const int PIN_D8_SIGNAL = 8;
inline void indicatorSetScrollActive(bool scrollActive) {
bool level;
if (INDICATOR_ACTIVE_LOW) level = scrollActive ? LOW : HIGH;
else level = scrollActive ? HIGH : LOW;
digitalWrite(PIN_D8_SIGNAL, level);
}
inline void indicatorSetNormal() { indicatorSetScrollActive(false); }
inline void indicatorSetScroll() { indicatorSetScrollActive(true); }
const int PIN_D2_LEFT = 2; // active LOW (INPUT_PULLUP)
const int PIN_D3_RIGHT = 3; // active LOW (INPUT_PULLUP)
const int PIN_A0_MODE = A0; // NO->GND (INPUT_PULLUP)
const int PIN_A1_MODE = A1; // ✅ NEW: NO->GND (INPUT_PULLUP) PRIORITY over A0 (LOCK)
const int PIN_D4_AUTO = 4; // AUTO active LOW (INPUT_PULLUP)
const int PIN_D13_RESET = 13; // PAUSE active LOW (INPUT_PULLUP)
// --------------------------- 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,
BEHAV_BLOCK = 8,
BEHAV_SCROLL = 9
};
BehaviorMode currentBehaviorMode = BEHAV_DEFAULT;
BehaviorMode previousBehaviorMode = BEHAV_DEFAULT;
// -------------------- Scroll Words state --------------------
const int NUM_SCROLLS = 9;
int scrollIndex = 0;
bool exitScrollMode = false;
// --------------------------- BUTTON / DEBOUNCE (A0 + A1) --------------------
struct ModeBtnState {
bool lastState;
bool pressLatched;
bool longLatched;
unsigned long lowStartMs;
};
ModeBtnState btnA0 = { HIGH, false, false, 0 };
ModeBtnState btnA1 = { HIGH, false, false, 0 };
const unsigned long MODE_DEBOUNCE_MS = 60;
const unsigned long MODE_LONG_MS = 3000;
// --------------------------- IDLE / BORED -----------------------------------
unsigned long lastActivityMs = 0;
const unsigned long BORED_MS = 15000;
bool modeChanged = false;
// -------------------------- D13 / PAUSE STATE -------------------------------
bool pausedByD13 = false;
// ✅ AUTO uses SAME animations as manual (but we allow manual funcs to run in AUTO)
bool gAutoPlayback = false;
// ============================================================================
// EYE PATTERNS (8x8 bitmaps)
// ============================================================================
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,
};
// ============================================================================
// 5x7 FONT IN PROGMEM (SRAM SAFE)
// ============================================================================
const uint8_t PROGMEM font5x7_A_to_Z[33][5] = {
{0x7E,0x11,0x11,0x11,0x7E}, {0x7F,0x49,0x49,0x49,0x36}, {0x3E,0x41,0x41,0x41,0x22},
{0x7F,0x41,0x41,0x22,0x1C}, {0x7F,0x49,0x49,0x49,0x41}, {0x7F,0x09,0x09,0x09,0x01},
{0x3E,0x41,0x49,0x49,0x7A}, {0x7F,0x08,0x08,0x08,0x7F}, {0x00,0x41,0x7F,0x41,0x00},
{0x20,0x40,0x41,0x3F,0x01}, {0x7F,0x08,0x14,0x22,0x41}, {0x7F,0x40,0x40,0x40,0x40},
{0x7F,0x02,0x04,0x02,0x7F}, {0x7F,0x04,0x08,0x10,0x7F}, {0x3E,0x41,0x41,0x41,0x3E},
{0x7F,0x09,0x09,0x09,0x06}, {0x3E,0x41,0x51,0x21,0x5E}, {0x7F,0x09,0x19,0x29,0x46},
{0x46,0x49,0x49,0x49,0x31}, {0x01,0x01,0x7F,0x01,0x01}, {0x3F,0x40,0x40,0x40,0x3F},
{0x1F,0x20,0x40,0x20,0x1F}, {0x3F,0x40,0x38,0x40,0x3F}, {0x63,0x14,0x08,0x14,0x63},
{0x07,0x08,0x70,0x08,0x07}, {0x61,0x51,0x49,0x45,0x43},
{0x02,0x01,0x59,0x09,0x06}, {0x00,0x00,0x5F,0x00,0x00}, {0x14,0x7F,0x14,0x7F,0x14},
{0x00,0x08,0x08,0x08,0x00}, {0x36,0x49,0x55,0x22,0x50}, {0x00,0x60,0x60,0x00,0x00},
{0x00,0x40,0x60,0x00,0x00}
};
// ============================================================================
// PROGMEM MESSAGES (SRAM SAFE)
// ============================================================================
const char PROGMEM MSG0[] = "HI THERE! MY NAME IS E-MAR.";
const char PROGMEM MSG1[] = "GOOD MORNING. I HOPE YOU ARE DOING WELL TODAY";
const char PROGMEM MSG2[] = "GOOD AFTERNOON! WISHING YOU A PLEASANT REST OF THE DAY";
const char PROGMEM MSG3[] = "YOUR PERSONALITY IS AMAZING, KEEP SHINING";
const char PROGMEM MSG4[] = "THANK YOU! HAVE A WONDERFUL DAY";
const char PROGMEM MSG5[] = "HOW MAY I ASSIST YOU?";
const char PROGMEM MSG6[] = "WOULD YOU MIND TO TALK WITH MY CREATOR";
const char PROGMEM MSG7[] = "GREAT CHAT! I HAVE TO HEAD OFF";
const char PROGMEM MSG8[] = "GOD BLESS US ALL.";
// ============================================================================
// FORWARD DECLARATIONS
// ============================================================================
bool isAuto();
void checkD13();
void handleExternalPause();
void updateModeButton();
void checkD2D3();
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 showRunningAutoOnce(unsigned long totalMs);
void displayIdleAnimation();
void showBoredAnimation();
void showReactionFace();
void displaySequenceLeft();
void displaySequenceRight();
void displayBehaviorDefault();
void displayBehaviorHeart();
void displayBehaviorSmall();
void displayBehaviorSleepy();
void displayBehaviorAngry();
void displayBehaviorMarvin();
void displayBehaviorQuizzagan();
void displayBehaviorOff();
void displayBehaviorBlock();
void displayBehaviorScrollWords();
void displayScrollAnim0();
void displayScrollAnim1();
void displayScrollAnim2();
void displayScrollAnim3();
void displayScrollAnim4();
void displayScrollAnim5();
void displayScrollAnim6();
void displayScrollAnim7();
void displayScrollAnim8();
void scrollText_P(const char* txtP, unsigned long speedMs);
void drawPupilFrame(int xL, int yL, int xR, int yR);
// -----------------------------------------------------------------------------
// INTERRUPTS
// -----------------------------------------------------------------------------
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]);
}
}
// -----------------------------------------------------------------------------
// HELPERS
// -----------------------------------------------------------------------------
bool isAuto() { return digitalRead(PIN_D4_AUTO) == LOW; }
// -----------------------------------------------------------------------------
// PRIORITY PAUSE (D13)
// -----------------------------------------------------------------------------
void checkD13() {
if (digitalRead(PIN_D13_RESET) == LOW) handleExternalPause();
}
void handleExternalPause() {
pausedByD13 = true;
bool wasScrolling = (currentBehaviorMode == BEHAV_SCROLL);
detachInterrupt(digitalPinToInterrupt(PIN_D2_LEFT));
detachInterrupt(digitalPinToInterrupt(PIN_D3_RIGHT));
indicatorSetNormal();
while (digitalRead(PIN_D13_RESET) == LOW) {
lc1.clearDisplay(0);
lc2.clearDisplay(0);
delay(80);
}
pausedByD13 = false;
attachInterrupt(digitalPinToInterrupt(PIN_D2_LEFT), handleLeftInterrupt, FALLING);
attachInterrupt(digitalPinToInterrupt(PIN_D3_RIGHT), handleRightInterrupt, FALLING);
if (wasScrolling) indicatorSetScroll();
else indicatorSetNormal();
lastActivityMs = millis();
}
// -----------------------------------------------------------------------------
// SMART DELAY (interruptible + auto-release fast exit)
// -----------------------------------------------------------------------------
void smartDelay(unsigned long ms, bool allowD2D3, bool breakOnModeChange) {
unsigned long start = millis();
while (millis() - start < ms) {
delay(1);
checkD13();
updateModeButton(); // ignores AUTO internally
if (allowD2D3) checkD2D3();
// If we are playing manual animations inside AUTO and AUTO is released -> exit immediately
if (gAutoPlayback && !isAuto()) return;
if (breakOnModeChange && modeChanged) return;
if (pausedByD13) return;
}
}
// -----------------------------------------------------------------------------
// A0 + A1 MODE BUTTON HANDLER (A1 PRIORITY + PRIORITY LOCK)
// -----------------------------------------------------------------------------
static inline void processModeButton(ModeBtnState &b, bool reading, bool allowActions) {
unsigned long now = millis();
// detect falling edge -> start timing
if (b.lastState == HIGH && reading == LOW) b.lowStartMs = now;
if (reading == LOW) {
// LONG press
if (!b.longLatched && (now - b.lowStartMs) >= MODE_LONG_MS) {
b.longLatched = true;
b.pressLatched = true;
if (!allowActions) { b.lastState = reading; return; }
modeChanged = true;
if (currentBehaviorMode != BEHAV_SCROLL) {
previousBehaviorMode = currentBehaviorMode;
scrollIndex = 0;
exitScrollMode = false;
lc1.clearDisplay(0); lc2.clearDisplay(0); delay(40);
showBoth(eyeM); smartDelay(350, false, false);
showBoth(eyeQ); smartDelay(350, false, false);
showBoth(eyeBlank); smartDelay(80, false, false);
currentBehaviorMode = BEHAV_SCROLL;
indicatorSetScroll();
displayBehaviorScrollWords();
modeChanged = false;
} else {
exitScrollMode = true;
currentBehaviorMode = previousBehaviorMode;
lc1.clearDisplay(0); lc2.clearDisplay(0); delay(40);
showBoth(eyeM); smartDelay(350, false, false);
showBoth(eyeQ); smartDelay(350, false, false);
showBoth(eyeBlank); smartDelay(80, false, false);
indicatorSetNormal();
}
}
// SHORT press (debounced)
else if (!b.pressLatched && (now - b.lowStartMs) >= MODE_DEBOUNCE_MS && !b.longLatched) {
b.pressLatched = true;
if (!allowActions) { b.lastState = reading; return; }
if (currentBehaviorMode == BEHAV_SCROLL) {
scrollIndex = (scrollIndex + 1) % NUM_SCROLLS;
modeChanged = true;
lastActivityMs = now;
} else {
currentBehaviorMode = (BehaviorMode)((currentBehaviorMode + 1) % 9);
modeChanged = true;
lastActivityMs = now;
indicatorSetNormal();
}
}
} else {
// released
b.pressLatched = false;
b.longLatched = false;
}
b.lastState = reading;
}
// ✅ PRIORITY LOCK helper
static inline void resetBtnState(ModeBtnState &b) {
b.lastState = HIGH;
b.pressLatched = false;
b.longLatched = false;
b.lowStartMs = 0;
}
void updateModeButton() {
if (isAuto() && !gAutoPlayback) return;
if (pausedByD13) return;
bool rA1 = digitalRead(PIN_A1_MODE);
bool rA0 = digitalRead(PIN_A0_MODE);
// ✅ A1 PRIORITY LOCK:
// While A1 is LOW, A0 is forced IDLE (cannot queue timing/latches).
if (rA1 == LOW) {
processModeButton(btnA1, rA1, true);
// HARD LOCK A0 (must be released and pressed again after A1 releases)
resetBtnState(btnA0);
return;
}
// A1 not active -> normal behavior (both work)
processModeButton(btnA1, rA1, true);
processModeButton(btnA0, rA0, true);
}
// -----------------------------------------------------------------------------
// D2 / D3 POLLING CHECK (manual only)
// -----------------------------------------------------------------------------
void checkD2D3() {
if (isAuto() && !gAutoPlayback) 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_A1_MODE, INPUT_PULLUP); // ✅ NEW
// FIX: D13 MUST NOT FLOAT
pinMode(PIN_D13_RESET, INPUT_PULLUP);
pinMode(PIN_D8_SIGNAL, OUTPUT);
lc1.shutdown(0, false); lc1.setIntensity(0, 8); lc1.clearDisplay(0);
lc2.shutdown(0, false); lc2.setIntensity(0, 8); lc2.clearDisplay(0);
indicatorSetNormal();
// ✅ IMPORTANT: A1 is now a BUTTON, so seed from another unused analog (ex: A2)
randomSeed(analogRead(A2));
attachInterrupt(digitalPinToInterrupt(PIN_D2_LEFT), handleLeftInterrupt, FALLING);
attachInterrupt(digitalPinToInterrupt(PIN_D3_RIGHT), handleRightInterrupt, FALLING);
lastActivityMs = millis();
}
// ============================================================================
// STREAMING SCROLL (NO 1024-BYTE BUFFER)
// ============================================================================
static inline uint8_t fontCol(char c, uint8_t col) {
if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
int fi = -1;
if (c >= 'A' && c <= 'Z') fi = c - 'A';
else {
switch (c) {
case '?': fi = 26; break;
case '!': fi = 27; break;
case '#': fi = 28; break;
case '-': fi = 29; break;
case '&': fi = 30; break;
case '.': fi = 31; break;
case ',': fi = 32; break;
default: fi = -1; break;
}
}
if (fi < 0) return 0x00;
return pgm_read_byte(&font5x7_A_to_Z[fi][col]);
}
static void renderWindow16(const uint8_t win[16]) {
byte leftBmp[8], rightBmp[8];
for (int r = 0; r < 8; r++) { leftBmp[r] = 0x00; rightBmp[r] = 0x00; }
for (int c = 0; c < 16; c++) {
uint8_t colData = win[c];
for (int row = 0; row < 7; row++) {
if ((colData & (1 << row)) == 0) continue;
int targetRow = row + 1; // vertical offset like your original
if (targetRow >= 8) continue;
if (c < 8) leftBmp[targetRow] |= (1 << (7 - c));
else rightBmp[targetRow] |= (1 << (7 - (c - 8)));
}
}
for (int r = 0; r < 8; r++) {
lc1.setRow(0, r, leftBmp[r]);
lc2.setRow(0, r, rightBmp[r]);
}
}
static inline void shiftPush(uint8_t win[16], uint8_t col) {
for (int i = 0; i < 15; i++) win[i] = win[i + 1];
win[15] = col;
}
void scrollText_P(const char* txtP, unsigned long speedMs) {
uint8_t win[16];
for (int i = 0; i < 16; i++) win[i] = 0x00;
// lead-in blanks
for (int i = 0; i < 16; i++) {
if (isAuto() && !gAutoPlayback) return;
if (modeChanged || exitScrollMode) return;
if (digitalRead(PIN_D13_RESET) == LOW) {
handleExternalPause();
if (isAuto() && !gAutoPlayback) return;
indicatorSetScroll();
}
shiftPush(win, 0x00);
renderWindow16(win);
smartDelay(speedMs, false, true);
if (pausedByD13) return;
}
// stream characters from PROGMEM
for (uint16_t idx = 0;; idx++) {
char c = (char)pgm_read_byte(&txtP[idx]);
if (c == '\0') break;
if (c == ' ') {
for (int s = 0; s < 6; s++) {
if (isAuto() && !gAutoPlayback) return;
if (modeChanged || exitScrollMode) return;
if (digitalRead(PIN_D13_RESET) == LOW) {
handleExternalPause();
if (isAuto() && !gAutoPlayback) return;
indicatorSetScroll();
}
shiftPush(win, 0x00);
renderWindow16(win);
smartDelay(speedMs, false, true);
if (pausedByD13) return;
}
continue;
}
// 5 columns + 1 spacer
for (int col = 0; col < 5; col++) {
if (isAuto() && !gAutoPlayback) return;
if (modeChanged || exitScrollMode) return;
if (digitalRead(PIN_D13_RESET) == LOW) {
handleExternalPause();
if (isAuto() && !gAutoPlayback) return;
indicatorSetScroll();
}
shiftPush(win, fontCol(c, col));
renderWindow16(win);
smartDelay(speedMs, false, true);
if (pausedByD13) return;
}
// spacer
if (isAuto() && !gAutoPlayback) return;
if (modeChanged || exitScrollMode) return;
shiftPush(win, 0x00);
renderWindow16(win);
smartDelay(speedMs, false, true);
if (pausedByD13) return;
}
// tail blanks
for (int i = 0; i < 16; i++) {
if (isAuto() && !gAutoPlayback) return;
if (modeChanged || exitScrollMode) return;
shiftPush(win, 0x00);
renderWindow16(win);
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 WORDS MODE
// ============================================================================
void displayScrollAnim0() { scrollText_P(MSG0, 110); }
void displayScrollAnim1() { scrollText_P(MSG1, 110); }
void displayScrollAnim2() { scrollText_P(MSG2, 110); }
void displayScrollAnim3() { scrollText_P(MSG3, 110); }
void displayScrollAnim4() { scrollText_P(MSG4, 110); }
void displayScrollAnim5() { scrollText_P(MSG5, 110); }
void displayScrollAnim6() { scrollText_P(MSG6, 110); }
void displayScrollAnim7() { scrollText_P(MSG7, 110); }
void displayScrollAnim8() { scrollText_P(MSG8, 110); }
void displayBehaviorScrollWords() {
// ✅ In AUTO playback, do ONE message then return (no infinite loop in AUTO)
if (gAutoPlayback) {
modeChanged = false;
exitScrollMode = false;
indicatorSetScroll();
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;
}
return;
}
modeChanged = false;
exitScrollMode = false;
detachInterrupt(digitalPinToInterrupt(PIN_D2_LEFT));
detachInterrupt(digitalPinToInterrupt(PIN_D3_RIGHT));
indicatorSetScroll();
while (!exitScrollMode) {
if (digitalRead(PIN_D13_RESET) == LOW) {
handleExternalPause();
if (isAuto() && !gAutoPlayback) break;
indicatorSetScroll();
}
if (isAuto() && !gAutoPlayback) break;
modeChanged = false;
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;
}
smartDelay(80, false, true);
}
attachInterrupt(digitalPinToInterrupt(PIN_D2_LEFT), handleLeftInterrupt, FALLING);
attachInterrupt(digitalPinToInterrupt(PIN_D3_RIGHT), handleRightInterrupt, FALLING);
if (exitScrollMode) {
indicatorSetNormal();
currentBehaviorMode = previousBehaviorMode;
exitScrollMode = false;
modeChanged = false;
lastActivityMs = millis();
} else {
indicatorSetScroll();
}
}
// ============================================================================
// AUTO MARQUEE + AUTO MODE
// ============================================================================
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);
}
void autoMode() {
leftTriggered = false;
rightTriggered = false;
detachInterrupt(digitalPinToInterrupt(PIN_D2_LEFT));
detachInterrupt(digitalPinToInterrupt(PIN_D3_RIGHT));
indicatorSetNormal();
// Optional AUTO banner
showRunningAutoOnce(autoRunMs);
unsigned long lastAutoIndicatorMs = millis();
// ✅ allow manual behavior functions to run while AUTO is active
gAutoPlayback = true;
while (isAuto()) {
checkD13();
if (millis() - lastAutoIndicatorMs >= autoIntervalMs) {
showRunningAutoOnce(autoRunMs);
lastAutoIndicatorMs = millis();
}
// ✅ SAME animation engine as manual:
displayIdleAnimation();
smartDelay(autoFrameGapMs, false, true);
if (modeChanged) modeChanged = false;
}
gAutoPlayback = false;
attachInterrupt(digitalPinToInterrupt(PIN_D2_LEFT), handleLeftInterrupt, FALLING);
attachInterrupt(digitalPinToInterrupt(PIN_D3_RIGHT), handleRightInterrupt, FALLING);
lastActivityMs = millis();
}
// ============================================================================
// MANUAL LEFT/RIGHT SEQUENCES
// ============================================================================
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;
default: showBoth(eyeOpen); smartDelay(180, false, false); break;
}
}
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 (normal-mode logic, but allowed to run in AUTO via gAutoPlayback)
// ============================================================================
void displayBehaviorDefault() {
modeChanged = false;
for (int i = 0; i < 30; i++) {
showBoth(eyeOpen);
smartDelay(60, true, true);
if (modeChanged || (isAuto() && !gAutoPlayback) || pausedByD13) return;
}
showBoth(eyeWink); smartDelay(40, true, true); if (modeChanged || (isAuto() && !gAutoPlayback) || pausedByD13) return;
showBoth(eyeClosed); smartDelay(40, true, true); if (modeChanged || (isAuto() && !gAutoPlayback) || pausedByD13) return;
showBoth(eyeClosedTotal); smartDelay(40, true, true); if (modeChanged || (isAuto() && !gAutoPlayback) || pausedByD13) return;
showBoth(eyeOpen); smartDelay(80, true, true);
}
void displayBehaviorHeart() {
modeChanged = false;
showPair(eyeHeart1, eyeHeart3); smartDelay(120, true, true); if (modeChanged) return;
if (isAuto() && !gAutoPlayback) return;
if (pausedByD13) return;
showPair(eyeHeart2, eyeHeart2); smartDelay(120, true, true); if (modeChanged) return;
if (isAuto() && !gAutoPlayback) return;
if (pausedByD13) return;
showPair(eyeHeart3, eyeHeart1); smartDelay(120, true, true); if (modeChanged) return;
if (isAuto() && !gAutoPlayback) return;
if (pausedByD13) return;
showPair(eyeBlank, eyeBlank); smartDelay(120, true, true); if (modeChanged) return;
if (isAuto() && !gAutoPlayback) return;
if (pausedByD13) return;
showPair(eyeHeart3, eyeHeart1); smartDelay(120, true, true); if (modeChanged) return;
if (isAuto() && !gAutoPlayback) return;
if (pausedByD13) return;
showPair(eyeHeart2, eyeHeart2); smartDelay(120, true, true); if (modeChanged) return;
if (isAuto() && !gAutoPlayback) return;
if (pausedByD13) return;
showPair(eyeHeart1, eyeHeart3); smartDelay(120, true, true); if (modeChanged) return;
if (isAuto() && !gAutoPlayback) 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 || (isAuto() && !gAutoPlayback) || pausedByD13) return;
showBoth(eyeSmall2); smartDelay(60, true, true); if (modeChanged || (isAuto() && !gAutoPlayback) || pausedByD13) return;
showBoth(eyeSmall3); smartDelay(60, true, true); if (modeChanged || (isAuto() && !gAutoPlayback) || pausedByD13) return;
showBoth(eyeSmall4); smartDelay(60, true, true); if (modeChanged || (isAuto() && !gAutoPlayback) || pausedByD13) return;
showBoth(eyeSmall5); smartDelay(60, true, true); if (modeChanged || (isAuto() && !gAutoPlayback) || pausedByD13) return;
showBoth(eyeSmall6); smartDelay(120, true, true);
}
void displayBehaviorMarvin() {
modeChanged = false;
showPair(eyeBlank, eyeM); smartDelay(marvinStepMs, true, true); if (modeChanged) return;
if (isAuto() && !gAutoPlayback) return;
if (pausedByD13) return;
showPair(eyeM, eyeA); smartDelay(marvinStepMs, true, true); if (modeChanged) return;
if (isAuto() && !gAutoPlayback) return;
if (pausedByD13) return;
showPair(eyeA, eyeR); smartDelay(marvinStepMs, true, true); if (modeChanged) return;
if (isAuto() && !gAutoPlayback) return;
if (pausedByD13) return;
showPair(eyeR, eyeV); smartDelay(marvinStepMs, true, true); if (modeChanged) return;
if (isAuto() && !gAutoPlayback) return;
if (pausedByD13) return;
showPair(eyeV, eyeI); smartDelay(marvinStepMs, true, true); if (modeChanged) return;
if (isAuto() && !gAutoPlayback) return;
if (pausedByD13) return;
showPair(eyeI, eyeN); smartDelay(marvinStepMs, true, true); if (modeChanged) return;
if (isAuto() && !gAutoPlayback) return;
if (pausedByD13) return;
showPair(eyeN, eyeBlank); smartDelay(marvinStepMs, true, true); if (modeChanged) return;
if (isAuto() && !gAutoPlayback) 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() && !gAutoPlayback) return;
if (pausedByD13) return;
showBoth(eyeSleep); smartDelay(400, true, true); if (modeChanged) return;
if (isAuto() && !gAutoPlayback) return;
if (pausedByD13) return;
showBoth(eyeClosed); smartDelay(400, true, true); if (modeChanged) return;
if (isAuto() && !gAutoPlayback) 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() && !gAutoPlayback) 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() && !gAutoPlayback) 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 || (isAuto() && !gAutoPlayback) || pausedByD13) return;
}
showPair(eyeBlank, eyeBlank);
smartDelay(quizzaganPauseMs, true, true);
}
void displayBehaviorOff() {
modeChanged = false;
if (isAuto() && !gAutoPlayback) { showBoth(eyeBlank); return; }
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));
// In AUTO playback, don't park here forever — just return quickly
if (gAutoPlayback) {
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);
return;
}
while (!modeChanged) {
if (isAuto() && !gAutoPlayback) {
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);
return;
}
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;
}
void displayBehaviorBlock() {
modeChanged = false;
// In AUTO playback, do ONE block "pulse" then return (keeps AUTO moving)
if (gAutoPlayback) {
if (random(0, 10) < 8) showBoth(eyeBlock);
else showBoth(eyeBlank);
smartDelay(220 + random(0, 120), false, true);
return;
}
while (!modeChanged) {
if (isAuto() && !gAutoPlayback) return;
if (random(0, 10) < 8) {
showBoth(eyeBlock);
smartDelay(400 + random(0, 200), false, true);
} else {
showBoth(eyeBlank);
smartDelay(100 + random(0, 150), false, true);
}
checkD2D3();
if (leftTriggered) {
leftTriggered = false;
displaySequenceLeft();
}
if (rightTriggered) {
rightTriggered = false;
displaySequenceRight();
}
smartDelay(300, true, true);
if (isAuto() && !gAutoPlayback) return;
}
}
// ============================================================================
// BORED / IDLE
// ============================================================================
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_SCROLL: displayBehaviorScrollWords(); break;
default: displayBehaviorDefault(); break;
}
}
// ============================================================================
// MAIN LOOP
// ============================================================================
void loop() {
checkD13();
if (isAuto()) {
BehaviorMode savedMode = currentBehaviorMode;
bool resumeScrollAfterAuto = (savedMode == BEHAV_SCROLL);
autoMode();
if (resumeScrollAfterAuto && !pausedByD13) {
currentBehaviorMode = BEHAV_SCROLL;
indicatorSetScroll();
displayBehaviorScrollWords();
} else {
currentBehaviorMode = savedMode;
modeChanged = false;
lastActivityMs = millis();
if (currentBehaviorMode != BEHAV_SCROLL) indicatorSetNormal();
}
return;
}
updateModeButton();
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 (used by angry swirl)
// ============================================================================
void drawPupilFrame(int xL, int yL, int xR, int yR) {
byte leftBmp[8], 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;
// ✅ BUG FIX: must check xx, not yy
if (xx < 0 || xx > 7) continue;
rightBmp[yy] |= (1 << (7 - xx));
}
}
showPair(leftBmp, rightBmp);
}