// ============================================================================
// CREATED AND DESIGNED BY: MARVIN A. QUIZZAGAN - LEFT/RIGHT DRIVER VIEW
// MARCH 3, 2026 - 100% WORKING - POWER & BUZZER DISPLAY RESTORE FIX
// UPDATED WITH NON-BLOCKING BUZZER PATTERNS AND D3 FIX
//
// ✅ MOD (MAR 03, 2026):
// 1) A2 AUDIO MODE: WHEN ACTIVATED, FORCE INITIAL LEVEL = LEVEL 3 (BEEP AND MP3)
// 2) ✅ NEW: A6 EMERGENCY SHUTDOWN SWITCH (PULL-UP EXTERNAL)
// - A6 DEFAULT HIGH (5V via pull-up) => ALL FUNCTIONS NORMAL
// - A6 LOW => IMMEDIATE SHUTDOWN (deactivateSystem)
// - System cannot be activated while A6 is LOW
//
// ✅ MOD (MAR 03, 2026 - NEW REQUEST):
// - While in SLEEP MODE (LCD backlight OFF), if D11/D10/D9/D6 activates:
// show the same alert graphics/text AND ALSO BEEP if buzzerEnabled (A3 enabled).
// (Previously alerts were blocked when buzzerEnabled == true)
//
// ✅ FIX / UPDATE (DEC 25, 2025):
// - A2 AUDIO MODE (ONLY WORKS WHEN BUZZER ENABLED VIA A3):
// * LONG PRESS A2 (tunable) toggles AUDIO MODE ON/OFF
// * ON: Shows "AUDIO MODE IS" / "ACTIVATED" (3s) then shows level (3s) then returns to normal display
// * OFF: Shows "AUDIO MODE IS" / "DEACTIVATED" (3s) then returns to normal display
// * While ON: display forced to Speed Level 6 (stable "MARVIN QUIZZAGAN"), timer continues running
// * While ON: SHORT PRESS A2 cycles levels immediately (BEEP ON PRESS ONLY)
// * Outputs per level:
// Level 1 (BEEP ONLY): D12=HIGH, D4=LOW, D2=LOW
// Level 2 (MP3 ONLY): D12=HIGH, D4=HIGH, D2=HIGH
// Level 3 (BEEP AND MP3): D12=HIGH, D4=LOW, D2=HIGH
//
// ✅ NO EXTRA BEEPS ON RELEASE (DEC 25, 2025):
// - A3 beeps ONLY on PRESS (HIGH->LOW) and is excluded from global A0-A3 beep engine
// - D9/D6 beeps ONLY on rising edge (LOW->HIGH) and uses LOW re-arm time to prevent bounce beeps
// - ✅ FIXED: When audio mode is DEACTIVATED while still holding A2, prevent "extra press beep"
// by suppressing A2 beeps until A2 is released
//
// ✅ TRUE RUNTIME TIMER (background):
// - TIMER shows how long system has been running since activation (millis()-activatedAtMs)/1000
// - Timer continues running even while messages are shown
//
// ✅ SLEEP ALERTS (when LCD is sleeping AND buzzer is DISABLED):
// - D11: "OBSTACLE" / "DETECTED!!!" + flashing
// - D10: "REVERSING!!!" alternating lines
// - D9: "TURNING TO" / "THE RIGHT" + huge RIGHT arrow flashing
// - D6: "TURNING TO" / "THE LEFT" + huge LEFT arrow flashing
// - Any A0-A3 press wakes and cancels alerts immediately
// ============================================================================
enum SleepAlertMode {
ALERT_NONE,
ALERT_D11_OBSTACLES,
ALERT_D10_REVERSING,
ALERT_D9_RIGHT,
ALERT_D6_LEFT
};
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>
#include <string.h>
#include <stdio.h>
// -------------------- Pins --------------------
#define PIN_A0 A0
#define PIN_A1 A1
#define PIN_A2 A2
#define PIN_A3 A3
#define PIN_EMERGENCY_A6 A6 // ✅ Emergency shutdown input (external pull-up => HIGH default)
#define PIN_OUT 13
// buzzer wiring
#define PIN_BUZZER_EN 3 // D3 -> MOSFET gate enable/disable only (HIGH=enabled)
#define PIN_BUZZER_TONE 5 // D5 -> passive piezo tone output pin
// --- INPUT TRIGGERS ---
#define PIN_IN_D11 11
#define PIN_IN_D10 10
#define PIN_IN_D9 9
#define PIN_IN_D6 6
#define PIN_IN_D7 7
#define PIN_IN_D8 8
// ✅ AUDIO MODE OUTPUTS (CONTROLLED BY A2 LEVELS)
#define PIN_AUDIO_D12 12
#define PIN_AUDIO_D4 4
#define PIN_AUDIO_D2 2
// -------------------- TUNABLES (BEEP ENGINE TIMINGS) --------------------
// A0-A2 one-shot beep:
unsigned long d5OneShotHighMs = 100; // <-- TUNABLE (ms)
// A0-A2 press lockout (fast press friendly):
unsigned long aStableLowMs = 30; // <-- TUNABLE (ms)
// D9/D6 single beep:
unsigned long d5SinglePulseMs = 100; // <-- TUNABLE (ms)
// D9/D6 edge lockout (0 = absolute instant)
unsigned long d9d6StableMs = 0; // <-- TUNABLE (ms)
// ✅ D9/D6 RE-ARM LOW TIME (filters release bounce so no extra beeps)
unsigned long d9d6RearmLowMs = 30; // <-- TUNABLE (ms) 20..60ms recommended
// D11 continuous beeping:
unsigned long d11PulseHighMs = 1000; // <-- TUNABLE
unsigned long d11PulseLowMs = 250; // <-- TUNABLE
// D10 continuous beeping:
unsigned long d10PulseHighMs = 250; // <-- TUNABLE
unsigned long d10PulseLowMs = 250; // <-- TUNABLE
// D7/D8 edge debounce:
const unsigned long D7D8_STABLE_MS = 30; // <-- TUNABLE
// ✅ A2 audio mode long press (tunable)
unsigned long a2LongPressMs = 5000UL; // <-- TUNABLE
// ✅ EMERGENCY A6 THRESHOLD + DEBOUNCE
// External pull-up means A6 ~ 1023 when OK, near 0 when emergency is pulled LOW.
const int EMERGENCY_OK_THRESHOLD = 700; // analogRead(A6) >= this => OK (HIGH)
const unsigned long EMERGENCY_TRIP_MS = 20; // must stay LOW this long to trip (noise filter)
// ✅ NEW: Sleep alert entry beep (when LCD wakes to show alert)
bool sleepAlertEntryBeepEnabled = true; // set false if you ever want LCD-only alerts
unsigned long sleepAlertEntryBeepCooldownMs = 600; // prevents rapid re-beeps if signal chatters
unsigned long lastSleepAlertBeepMs = 0;
// -------------------- LCD --------------------
LiquidCrystal_I2C *lcd = nullptr;
// -------------------- EEPROM --------------------
const int EEPROM_LONGPRESS_ADDR = 0;
const unsigned long DEFAULT_LONGPRESS_MS = 5000UL;
unsigned long longPressMs = DEFAULT_LONGPRESS_MS;
// -------------------- Timing --------------------
const unsigned long DEBOUNCE_MS = 50;
// -------------------- Power State --------------------
bool active = false;
unsigned long bothPressedStart = 0;
bool bothPreviouslyPressed = false;
// -------------------- READY SEQUENCE --------------------
const unsigned long READY_SHOW_MS = 15000UL;
unsigned long activatedAtMs = 0;
bool showingReady = false;
const char readyScrollText[] = " READY!!! ";
const unsigned long READY_SCROLL_INTERVAL_MS = 400UL;
int readyScrollLen = sizeof(readyScrollText) - 1;
unsigned long lastReadyScrollMs = 0;
int readyScrollIndex = 0;
// -------------------- READY DOTS --------------------
unsigned long lastDotsMs = 0;
const unsigned long DOTS_INTERVAL_MS = 400UL;
int dotsCount = 0;
// -------------------- TRUE RUNTIME TIMER --------------------
unsigned long timerSeconds = 0;
// -------------------- Name Scroll --------------------
const char scrollTextSrc[] = " DESIGNED & CREATED BY: MARVIN AMORATO QUIZZAGAN ";
int scrollLen = sizeof(scrollTextSrc) - 1;
unsigned long lastScrollMs = 0;
int scrollIndex = 0;
const char staticName16[] = "MARVIN QUIZZAGAN";
// -------------------- Scroll Speeds --------------------
const unsigned long scrollSpeedsMs[6] = {1200UL, 800UL, 600UL, 400UL, 250UL, 0UL};
int scrollSpeedIndex = 2;
unsigned long scrollIntervalMs = scrollSpeedsMs[scrollSpeedIndex];
// -------------------- SPD Indicator --------------------
const unsigned long SPD_DISPLAY_MS = 1000UL;
unsigned long speedDisplayUntilMs = 0;
// -------------------- Blink --------------------
bool blinkState = true;
unsigned long lastBlinkMs = 0;
const unsigned long BLINK_INTERVAL_MS = 500UL;
// -------------------- Auto Sleep --------------------
const unsigned long AUTO_DIM_IDLE_MS = 30000UL; // 300000UL
bool displayOff = false;
unsigned long lastActivityMs = 0;
// -------------------- BUZZER --------------------
const unsigned long BUZZER_LONGPRESS_MS = 5000UL;
const unsigned long BUZZER_MSG_MS = 2000UL;
bool buzzerEnabled = false; // user state
bool buzzerMsgActive = false;
unsigned long buzzerMsgUntilMs = 0;
// -------------------- A3 Debounce --------------------
int lastA3StableState = HIGH;
int lastA3Read = HIGH;
unsigned long lastA3ChangeMs = 0;
unsigned long a3PressStartMs = 0;
bool a3LongPressHandled = false;
// Tone UI message time
const unsigned long TONE_MSG_MS = 2000UL;
// -------------------- Display Backup --------------------
char prevLine0[17] = " ";
char prevLine1[17] = " ";
// ✅ Audio/A2 message time
const unsigned long AUDIO_MSG_MS = 3000UL;
// -------------------- A2 AUDIO MODE STATE --------------------
bool a2AudioControlMode = false;
int savedScrollSpeedIndex = 2;
// 0=BEEP ONLY, 1=MP3 ONLY, 2=BEEP AND MP3
uint8_t audioLevel = 2;
// ✅ Prevent extra A2 beep after toggling audio mode while still holding A2
bool a2SuppressBeepUntilRelease = false;
// A2 press state (no beep on release)
int lastA2StableState = HIGH;
int lastA2Read = HIGH;
unsigned long lastA2ChangeMs = 0;
unsigned long a2PressStartMs = 0;
bool a2LongPressHandled = false;
// Header then level display sequencing (activation flow)
bool a2AudioHeaderPending = false;
uint8_t a2AudioPendingLevel = 0;
// -------------------- SLEEP ALERT STATE --------------------
SleepAlertMode alertMode = ALERT_NONE;
bool alertWasSleeping = false;
bool alertFlashOn = true;
unsigned long alertLastFlashMs = 0;
const unsigned long ALERT_FLASH_MS = 400; // <-- TUNABLE
// -------------------- EMERGENCY A6 STATE --------------------
bool emergencyOkStable = true;
unsigned long emergencyLowSinceMs = 0;
// -------------------- BUZZER PATTERN ENGINE --------------------
enum BuzzerPattern {
BUZZ_NONE,
BUZZ_STARTUP,
BUZZ_SHUTDOWN,
BUZZ_ENABLE,
BUZZ_DISABLE,
BUZZ_D7_ON,
BUZZ_D7_OFF,
BUZZ_D8_ON,
BUZZ_D8_OFF,
BUZZ_A2_AUDIO_ON, // rising
BUZZ_A2_AUDIO_OFF // falling
};
BuzzerPattern buzzerPattern = BUZZ_NONE;
int buzzerStep = 0;
bool buzzerActive = false;
// ✅ TONE STAGES (pitch control)
enum ToneStage { TONE_STAGE_LOW = 0, TONE_STAGE_MED = 1, TONE_STAGE_HIGH = 2 };
ToneStage toneStage = TONE_STAGE_MED;
// LOW/MED/HIGH multipliers (tunable)
const float toneMult[3] = { 0.80f, 1.00f, 1.20f };
// Current stage frequency helper
uint16_t toneFreq(uint16_t baseHz) {
float f = baseHz * toneMult[(int)toneStage];
if (f < 50) f = 50;
if (f > 8000) f = 8000;
return (uint16_t)(f + 0.5f);
}
// ✅ D3 drive rule: patterns can temporarily force-enable D3 so they can be heard
bool buzzerGateAllow() {
if (buzzerEnabled) return true;
return buzzerActive && (buzzerPattern != BUZZ_NONE);
}
void applyBuzzerEnablePin() {
digitalWrite(PIN_BUZZER_EN, buzzerGateAllow() ? HIGH : LOW);
}
// -------------------- MELODY DEFINITIONS (PASSIVE PIEZO) --------------------
struct NoteStep { uint16_t baseHz; uint16_t onMs; uint16_t offMs; };
// ✅ Startup
const NoteStep STARTUP_MELODY[] = {
{ 1800, 40, 15 }, { 2400, 55, 35 },
{ 2100, 35, 12 }, { 2500, 35, 12 }, { 2900, 45, 25 },
{ 2600, 35, 12 }, { 2200, 70, 55 },
{ 3200, 28, 14 }, { 2400, 55, 35 },
{ 2600, 28, 10 }, { 3000, 28, 18 }, { 2800, 30, 10 }, { 3600, 120, 0 }
};
const uint8_t STARTUP_LEN = sizeof(STARTUP_MELODY) / sizeof(STARTUP_MELODY[0]);
// ✅ Shutdown
const NoteStep SHUTDOWN_MELODY[] = {
{ 2400, 45, 15 }, { 2100, 65, 35 },
{ 2300, 40, 12 }, { 1900, 80, 60 },
{ 2600, 22, 20 },
{ 1700, 55, 12 }, { 1400, 110, 0 }
};
const uint8_t SHUTDOWN_LEN = sizeof(SHUTDOWN_MELODY) / sizeof(SHUTDOWN_MELODY[0]);
// ✅ Robot “ENABLED” / “DISABLED” (A3 long-press toggle)
const NoteStep ENABLE_MELODY[] = {
{ 2600, 35, 10 }, { 3200, 45, 30 },
{ 2900, 30, 10 }, { 3600, 35, 15 },
{ 2400, 90, 0 }
};
const uint8_t ENABLE_LEN = sizeof(ENABLE_MELODY) / sizeof(ENABLE_MELODY[0]);
const NoteStep DISABLE_MELODY[] = {
{ 1800, 55, 20 }, { 1500, 70, 35 },
{ 1300, 45, 12 }, { 1000, 120, 0 }
};
const uint8_t DISABLE_LEN = sizeof(DISABLE_MELODY) / sizeof(DISABLE_MELODY[0]);
// ✅ D7 unique robot talk (LOW->HIGH = ON, HIGH->LOW = OFF)
const NoteStep D7_ON_MELODY[] = { {1700,35,10}, {2100,35,10}, {2500,55,25}, {3100,35,15}, {2300,80,0} };
const uint8_t D7_ON_LEN = sizeof(D7_ON_MELODY)/sizeof(D7_ON_MELODY[0]);
const NoteStep D7_OFF_MELODY[] = { {2300,45,12}, {1900,65,20}, {1500,90,0} };
const uint8_t D7_OFF_LEN = sizeof(D7_OFF_MELODY)/sizeof(D7_OFF_MELODY[0]);
// ✅ D8 unique robot talk (LOW->HIGH = ON, HIGH->LOW = OFF)
const NoteStep D8_ON_MELODY[] = { {2600,30,10}, {3300,30,10}, {3900,45,20}, {3000,80,0} };
const uint8_t D8_ON_LEN = sizeof(D8_ON_MELODY)/sizeof(D8_ON_MELODY[0]);
const NoteStep D8_OFF_MELODY[] = { {2000,40,10}, {1600,60,15}, {1200,120,0} };
const uint8_t D8_OFF_LEN = sizeof(D8_OFF_MELODY)/sizeof(D8_OFF_MELODY[0]);
// ✅ A2 Audio mode activation/deactivation tones
const NoteStep A2_AUDIO_ON_MELODY[] = { {1600,45,10}, {2200,55,15}, {3000,90,0} }; // rising
const uint8_t A2_AUDIO_ON_LEN = sizeof(A2_AUDIO_ON_MELODY)/sizeof(A2_AUDIO_ON_MELODY[0]);
const NoteStep A2_AUDIO_OFF_MELODY[] = { {3000,45,10}, {2200,55,15}, {1600,100,0} }; // falling
const uint8_t A2_AUDIO_OFF_LEN = sizeof(A2_AUDIO_OFF_MELODY)/sizeof(A2_AUDIO_OFF_MELODY[0]);
bool melodyStepStarted = false;
unsigned long melodyStepEndsAt = 0;
// -------------------- D7/D8 EDGE STATE --------------------
int d7LastRead = LOW;
int d7Stable = LOW;
unsigned long d7ChangeMs = 0;
int d8LastRead = LOW;
int d8Stable = LOW;
unsigned long d8ChangeMs = 0;
// -------------------- BEEP ENGINE --------------------
enum D5Mode { D5_MODE_IDLE, D5_MODE_ONESHOT, D5_MODE_PULSE_D11, D5_MODE_PULSE_D10 };
D5Mode d5Mode = D5_MODE_IDLE;
unsigned long d5OneShotUntilMs = 0;
bool d5SinglePulseActive = false;
unsigned long d5SinglePulseUntilMs = 0;
// D9/D6 state
bool d9d6LastRaw = false;
bool d9d6Armed = true;
// D11/D10 pulse state
bool d5State = false;
unsigned long d5NextToggleMs = 0;
// A0-A3 immediate edge detect (we will SKIP A3 in the beep engine)
const uint8_t A_COUNT = 4;
const uint8_t A_PINS[A_COUNT] = { PIN_A0, PIN_A1, PIN_A2, PIN_A3 };
int aLastRaw[A_COUNT] = { HIGH, HIGH, HIGH, HIGH };
bool aArmed[A_COUNT] = { true, true, true, true };
unsigned long aLastTrigMs[A_COUNT] = { 0, 0, 0, 0 };
// -------------------- Sound ON/OFF using D5 (obeys D3 gate) --------------------
void buzzerSoundOnStage() {
applyBuzzerEnablePin();
if (!buzzerGateAllow()) { noTone(PIN_BUZZER_TONE); return; }
tone(PIN_BUZZER_TONE, toneFreq(1800));
}
void buzzerSoundOff() { noTone(PIN_BUZZER_TONE); }
// -------------------- EMERGENCY HELPERS --------------------
bool emergencyOkNow() {
int v = analogRead(PIN_EMERGENCY_A6);
return (v >= EMERGENCY_OK_THRESHOLD);
}
// Forward declarations used by emergency update
void deactivateSystem();
// ✅ Call this EVERY LOOP (very early) so shutdown is immediate.
void updateEmergencyShutdown() {
unsigned long now = millis();
bool ok = emergencyOkNow();
if (ok) {
emergencyOkStable = true;
emergencyLowSinceMs = 0;
return;
}
// Not OK (LOW): start / continue low timer
if (emergencyLowSinceMs == 0) emergencyLowSinceMs = now;
if ((now - emergencyLowSinceMs) >= EMERGENCY_TRIP_MS) {
emergencyOkStable = false;
// IMMEDIATE SHUTDOWN if running
if (active) {
deactivateSystem();
}
// Reset activation long-press tracking so it doesn't auto-toggle when released
bothPreviouslyPressed = false;
}
}
// -------------------- INPUT HELPERS --------------------
bool anyA_LowPressed() {
return (digitalRead(PIN_A0) == LOW) ||
(digitalRead(PIN_A1) == LOW) ||
(digitalRead(PIN_A2) == LOW) ||
(digitalRead(PIN_A3) == LOW);
}
bool anyD9D6High() {
return (digitalRead(PIN_IN_D9) == HIGH) ||
(digitalRead(PIN_IN_D6) == HIGH);
}
bool anyButtonPressed() { return anyA_LowPressed(); }
void wakeDisplay() {
if (!lcd) return;
lcd->backlight();
displayOff = false;
lastActivityMs = millis();
}
void forceSleepNow() {
if (!lcd) return;
lcd->clear();
lcd->noBacklight();
displayOff = true;
}
// -------------------- Backlight Flicker --------------------
void rampFlickerBacklight(bool startup = true) {
if (!lcd) return;
int steps = 20;
if (startup) {
for (int i = 0; i < steps; i++) {
random(0, 10) < 2 ? lcd->noBacklight() : lcd->backlight();
delay(random(30, 60));
}
lcd->backlight();
} else {
for (int i = steps; i >= 0; i--) {
int onTime = map(i, 0, steps, 0, 50);
lcd->backlight(); delay(onTime);
lcd->noBacklight(); delay(50 - onTime);
}
lcd->noBacklight();
}
}
// -------------------- READY DISPLAY --------------------
void showReadyMessageInit() {
if (!lcd) return;
lcd->clear();
dotsCount = 0;
readyScrollIndex = 0;
lastDotsMs = lastReadyScrollMs = millis();
lcd->setCursor(0, 0);
lcd->print("E-MAR IS NOW");
lcd->setCursor(13, 0);
lcd->print("...");
char out[17];
for (int i = 0; i < 16; i++) out[i] = readyScrollText[i % readyScrollLen];
out[16] = '\0';
lcd->setCursor(0, 1);
lcd->print(out);
}
void updateReadyScrolling() {
if (!lcd) return;
if (millis() - lastReadyScrollMs >= READY_SCROLL_INTERVAL_MS) {
lastReadyScrollMs = millis();
readyScrollIndex = (readyScrollIndex + 1) % readyScrollLen;
char out[17];
for (int i = 0; i < 16; i++)
out[i] = readyScrollText[(readyScrollIndex + i) % readyScrollLen];
out[16] = '\0';
lcd->setCursor(0, 1);
lcd->print(out);
}
}
void updateReadyDots() {
if (!lcd) return;
if (millis() - lastDotsMs >= DOTS_INTERVAL_MS) {
lastDotsMs = millis();
dotsCount = (dotsCount + 1) % 4;
lcd->setCursor(13, 0);
for (int i = 0; i < dotsCount; i++) lcd->print(".");
for (int i = dotsCount; i < 3; i++) lcd->print(" ");
}
}
// -------------------- TEXT HELPERS --------------------
void printLeftAligned12(int row, const char* s) {
if (!lcd) return;
char buf[13];
int len = (int)strlen(s);
if (len > 12) len = 12;
for (int i = 0; i < 12; i++) buf[i] = ' ';
for (int i = 0; i < len; i++) buf[i] = s[i];
buf[12] = '\0';
lcd->setCursor(0, row);
lcd->print(buf);
}
void printRightAligned12_fromCol4(int row, const char* s) {
if (!lcd) return;
char buf[13];
int len = (int)strlen(s);
if (len > 12) len = 12;
for (int i = 0; i < 12; i++) buf[i] = ' ';
int start = 12 - len;
for (int i = 0; i < len; i++) buf[start + i] = s[i];
buf[12] = '\0';
lcd->setCursor(4, row);
lcd->print(buf);
}
void printCentered16(const char* s) {
if (!lcd) return;
char buf[17];
int len = (int)strlen(s);
if (len > 16) len = 16;
int left = (16 - len) / 2;
for (int i = 0; i < 16; i++) buf[i] = ' ';
for (int i = 0; i < len; i++) buf[left + i] = s[i];
buf[16] = '\0';
lcd->print(buf);
}
// -------------------- Display Helpers --------------------
void showScrollingName() {
if (!lcd) return;
char out[17];
if (scrollSpeedIndex == 5) snprintf(out, 17, "%-16s", staticName16);
else for (int i = 0; i < 16; i++) out[i] = scrollTextSrc[(scrollIndex + i) % scrollLen];
out[16] = '\0';
lcd->setCursor(0, 0);
lcd->print(out);
}
void showTimerOrSpd(unsigned long sec, bool spd, int s) {
if (!lcd) return;
char buf[9];
unsigned long h = sec / 3600UL;
unsigned long m = (sec % 3600UL) / 60UL;
unsigned long se = sec % 60UL;
snprintf(buf, 9, "%02lu:%02lu:%02lu", h, m, se);
if (!blinkState) { buf[2] = ' '; buf[5] = ' '; }
lcd->setCursor(0, 1);
lcd->print(spd ? "SPD:" : "TIMER:");
if (spd) lcd->print(s + 1);
lcd->setCursor(6, 1);
lcd->print(buf);
}
// -------------------- Display Save/Restore for Messages --------------------
void capturePrevDisplayForRestore() {
if (showingReady) {
strcpy(prevLine0, "E-MAR IS NOW");
char out[17];
for (int i = 0; i < 16; i++) out[i] = readyScrollText[(readyScrollIndex + i) % readyScrollLen];
out[16] = '\0';
strcpy(prevLine1, out);
} else {
char out0[17];
if (scrollSpeedIndex == 5) snprintf(out0, 17, "%-16s", staticName16);
else {
for (int i = 0; i < 16; i++) out0[i] = scrollTextSrc[(scrollIndex + i) % scrollLen];
out0[16] = '\0';
}
strcpy(prevLine0, out0);
unsigned long sec = timerSeconds;
char buf[17];
snprintf(buf, 17, "%02lu:%02lu:%02lu", sec/3600, (sec%3600)/60, sec%60);
strcpy(prevLine1, buf);
}
}
void showToneStageMessage() {
if (!lcd) return;
capturePrevDisplayForRestore();
const char* label =
(toneStage == TONE_STAGE_LOW) ? "LOW" :
(toneStage == TONE_STAGE_MED) ? "MEDIUM" : "HIGH";
char line0[17];
snprintf(line0, 17, "TONE: %s", label);
char line1[17];
char bars[4] = {' ', ' ', ' ', '\0'};
for (int i = 0; i < 3; i++) bars[i] = (i <= (int)toneStage) ? (char)255 : ' ';
snprintf(line1, 17, "LEVEL: %c%c%c", bars[0], bars[1], bars[2]);
if (displayOff) wakeDisplay();
lcd->clear();
lcd->setCursor(0, 0); printCentered16(line0);
lcd->setCursor(0, 1); printCentered16(line1);
buzzerMsgActive = true;
buzzerMsgUntilMs = millis() + TONE_MSG_MS;
}
// -------------------- AUDIO MODE OUTPUTS + MESSAGES --------------------
void applyAudioOutputs(uint8_t lvl) {
digitalWrite(PIN_AUDIO_D12, LOW);
digitalWrite(PIN_AUDIO_D4, LOW);
digitalWrite(PIN_AUDIO_D2, LOW);
// Level mapping:
// 0) BEEP ONLY: D12=HIGH, D4=LOW, D2=LOW
// 1) MP3 ONLY: D12=HIGH, D4=HIGH, D2=HIGH
// 2) BEEP+MP3: D12=HIGH, D2=HIGH, D4=LOW
if (lvl == 0) {
digitalWrite(PIN_AUDIO_D12, HIGH);
} else if (lvl == 1) {
digitalWrite(PIN_AUDIO_D12, HIGH);
digitalWrite(PIN_AUDIO_D4, HIGH);
digitalWrite(PIN_AUDIO_D2, HIGH);
} else {
digitalWrite(PIN_AUDIO_D12, HIGH);
digitalWrite(PIN_AUDIO_D2, HIGH);
digitalWrite(PIN_AUDIO_D4, LOW);
}
}
void showAudioModeLevelMessage(uint8_t lvl) {
if (!lcd) return;
capturePrevDisplayForRestore();
const char* line2 =
(lvl == 0) ? "BEEP ONLY" :
(lvl == 1) ? "MP3 ONLY" :
"BEEP AND MP3";
if (displayOff) wakeDisplay();
lcd->clear();
lcd->setCursor(0, 0); printCentered16("AUDIO MODE:");
lcd->setCursor(0, 1); printCentered16(line2);
buzzerMsgActive = true;
buzzerMsgUntilMs = millis() + AUDIO_MSG_MS;
}
void showAudioModeActivatedHeader(uint8_t lvl) {
if (!lcd) return;
capturePrevDisplayForRestore();
if (displayOff) wakeDisplay();
lcd->clear();
lcd->setCursor(0, 0); printCentered16("BEEP+MP3 MODE IS");
lcd->setCursor(0, 1); printCentered16("ACTIVATED");
// after 3s, auto-show level message
a2AudioHeaderPending = true;
a2AudioPendingLevel = lvl;
buzzerMsgActive = true;
buzzerMsgUntilMs = millis() + AUDIO_MSG_MS;
}
void showAudioModeDeactivatedMessage() {
if (!lcd) return;
capturePrevDisplayForRestore();
if (displayOff) wakeDisplay();
lcd->clear();
lcd->setCursor(0, 0); printCentered16("BEEP+MP3 MODE IS");
lcd->setCursor(0, 1); printCentered16("DEACTIVATED");
buzzerMsgActive = true;
buzzerMsgUntilMs = millis() + AUDIO_MSG_MS;
}
// -------------------- Buzzer Control --------------------
void playBuzzerPattern(BuzzerPattern pattern) {
buzzerPattern = pattern;
buzzerStep = 0;
buzzerActive = true;
melodyStepStarted = false;
melodyStepEndsAt = 0;
applyBuzzerEnablePin();
}
void updateBuzzer() {
if (!buzzerActive) {
applyBuzzerEnablePin();
return;
}
unsigned long now = millis();
auto runMelody = [&](const NoteStep* melody, uint8_t len) {
if (buzzerStep >= len) { buzzerActive = false; return; }
if (!melodyStepStarted) {
const NoteStep &s = melody[buzzerStep];
applyBuzzerEnablePin();
if (buzzerGateAllow()) tone(PIN_BUZZER_TONE, toneFreq(s.baseHz), s.onMs);
else noTone(PIN_BUZZER_TONE);
melodyStepEndsAt = now + (unsigned long)s.onMs + (unsigned long)s.offMs;
melodyStepStarted = true;
}
if ((long)(now - melodyStepEndsAt) >= 0) {
buzzerStep++;
melodyStepStarted = false;
}
};
switch (buzzerPattern) {
case BUZZ_STARTUP: runMelody(STARTUP_MELODY, STARTUP_LEN); break;
case BUZZ_SHUTDOWN: runMelody(SHUTDOWN_MELODY, SHUTDOWN_LEN); break;
case BUZZ_ENABLE: runMelody(ENABLE_MELODY, ENABLE_LEN); break;
case BUZZ_DISABLE: runMelody(DISABLE_MELODY, DISABLE_LEN); break;
case BUZZ_D7_ON: runMelody(D7_ON_MELODY, D7_ON_LEN); break;
case BUZZ_D7_OFF: runMelody(D7_OFF_MELODY, D7_OFF_LEN); break;
case BUZZ_D8_ON: runMelody(D8_ON_MELODY, D8_ON_LEN); break;
case BUZZ_D8_OFF: runMelody(D8_OFF_MELODY, D8_OFF_LEN); break;
case BUZZ_A2_AUDIO_ON: runMelody(A2_AUDIO_ON_MELODY, A2_AUDIO_ON_LEN); break;
case BUZZ_A2_AUDIO_OFF: runMelody(A2_AUDIO_OFF_MELODY, A2_AUDIO_OFF_LEN); break;
default: buzzerActive = false; break;
}
if (!buzzerActive) {
noTone(PIN_BUZZER_TONE);
applyBuzzerEnablePin();
}
}
// -------------------- D7/D8 EDGE ROBOT TONES --------------------
void updateD7D8RobotTones() {
unsigned long now = millis();
if (!active) return;
if (!buzzerEnabled) return;
if (buzzerActive) return;
int r7 = digitalRead(PIN_IN_D7);
if (r7 != d7LastRead) { d7LastRead = r7; d7ChangeMs = now; }
if ((now - d7ChangeMs) >= D7D8_STABLE_MS && r7 != d7Stable) {
int prev = d7Stable;
d7Stable = r7;
if (prev == LOW && d7Stable == HIGH) playBuzzerPattern(BUZZ_D7_ON);
else if (prev == HIGH && d7Stable == LOW) playBuzzerPattern(BUZZ_D7_OFF);
return;
}
int r8 = digitalRead(PIN_IN_D8);
if (r8 != d8LastRead) { d8LastRead = r8; d8ChangeMs = now; }
if ((now - d8ChangeMs) >= D7D8_STABLE_MS && r8 != d8Stable) {
int prev = d8Stable;
d8Stable = r8;
if (prev == LOW && d8Stable == HIGH) playBuzzerPattern(BUZZ_D8_ON);
else if (prev == HIGH && d8Stable == LOW) playBuzzerPattern(BUZZ_D8_OFF);
return;
}
}
// -------------------- BEEP ENGINE UPDATE --------------------
void startD5OneShot(unsigned long now) {
d5Mode = D5_MODE_ONESHOT;
d5OneShotUntilMs = now + d5OneShotHighMs;
buzzerSoundOnStage();
}
void startD5ContinuousPulse_D11(unsigned long now) {
d5Mode = D5_MODE_PULSE_D11;
d5State = true;
buzzerSoundOnStage();
d5NextToggleMs = now + d11PulseHighMs;
}
void startD5ContinuousPulse_D10(unsigned long now) {
d5Mode = D5_MODE_PULSE_D10;
d5State = true;
buzzerSoundOnStage();
d5NextToggleMs = now + d10PulseHighMs;
}
void updateD5Output() {
unsigned long now = millis();
if (buzzerActive) return;
applyBuzzerEnablePin();
if (!buzzerEnabled) {
buzzerSoundOff();
d5Mode = D5_MODE_IDLE;
d5SinglePulseActive = false;
return;
}
static unsigned long lastAcceptedRiseMs = 0;
static unsigned long d9d6LowSinceMs = 0;
bool d9d6Now = anyD9D6High();
if (d9d6Now != d9d6LastRaw) {
d9d6LastRaw = d9d6Now;
if (d9d6Now) {
if (d9d6Armed && !d5SinglePulseActive) {
if (d9d6StableMs == 0 || (now - lastAcceptedRiseMs) >= d9d6StableMs) {
lastAcceptedRiseMs = now;
d5SinglePulseActive = true;
d5SinglePulseUntilMs = now + d5SinglePulseMs;
buzzerSoundOnStage();
d9d6Armed = false;
}
}
} else {
d9d6LowSinceMs = now;
}
}
if (!d9d6Now && !d9d6Armed) {
if ((now - d9d6LowSinceMs) >= d9d6RearmLowMs) d9d6Armed = true;
}
if (d5SinglePulseActive) {
if (now >= d5SinglePulseUntilMs) {
d5SinglePulseActive = false;
buzzerSoundOff();
}
return;
}
for (uint8_t i = 0; i < A_COUNT; i++) {
if (A_PINS[i] == PIN_A3) continue;
if (A_PINS[i] == PIN_A2 && (a2AudioControlMode || a2SuppressBeepUntilRelease)) continue;
int r = digitalRead(A_PINS[i]);
if (r != aLastRaw[i]) aLastRaw[i] = r;
if (r == HIGH) { aArmed[i] = true; continue; }
if (aArmed[i]) {
if (now - aLastTrigMs[i] >= aStableLowMs) {
aLastTrigMs[i] = now;
aArmed[i] = false;
startD5OneShot(now);
}
}
}
if (d5Mode == D5_MODE_ONESHOT) {
if (now >= d5OneShotUntilMs) {
buzzerSoundOff();
d5Mode = D5_MODE_IDLE;
} else {
return;
}
}
bool d11 = (digitalRead(PIN_IN_D11) == HIGH);
bool d10 = (digitalRead(PIN_IN_D10) == HIGH);
if (!d11 && !d10) {
d5Mode = D5_MODE_IDLE;
d5State = false;
buzzerSoundOff();
return;
}
if (d11) {
if (d5Mode != D5_MODE_PULSE_D11) { startD5ContinuousPulse_D11(now); return; }
if ((long)(now - d5NextToggleMs) >= 0) {
d5State = !d5State;
if (d5State) buzzerSoundOnStage(); else buzzerSoundOff();
d5NextToggleMs = now + (d5State ? d11PulseHighMs : d11PulseLowMs);
}
return;
}
if (d5Mode != D5_MODE_PULSE_D10) { startD5ContinuousPulse_D10(now); return; }
if ((long)(now - d5NextToggleMs) >= 0) {
d5State = !d5State;
if (d5State) buzzerSoundOnStage(); else buzzerSoundOff();
d5NextToggleMs = now + (d5State ? d10PulseHighMs : d10PulseLowMs);
}
}
// ============================================================================
// HUGE ARROWS (8 CELLS TOTAL) 4 columns wide x 2 rows
// ============================================================================
// *** LEFT/RIGHT DRIVER VIEW ***
byte R_T0[8] = { B00000,B00000,B00000,B00000,B00000,B00000,B00000,B00001 };
byte R_T1[8] = { B00000,B00000,B00001,B00011,B00111,B01111,B11111,B11111 };
byte R_T2[8] = { B00000,B00000,B00000,B00000,B00000,B00000,B11111,B11111 };
byte R_T3[8] = { B00000,B00000,B00000,B00000,B00000,B00000,B11111,B11111 };
byte R_B0[8] = { B00001,B00000,B00000,B00000,B00000,B00000,B00000,B00000 };
byte R_B1[8] = { B11111,B11111,B01111,B00111,B00011,B00001,B00000,B00000 };
byte R_B2[8] = { B11111,B11111,B00000,B00000,B00000,B00000,B00000,B00000 };
byte R_B3[8] = { B11111,B11111,B00000,B00000,B00000,B00000,B00000,B00000 };
byte L_T0[8] = { B00000,B00000,B00000,B00000,B00000,B00000,B11111,B11111 };
byte L_T1[8] = { B00000,B00000,B00000,B00000,B00000,B00000,B11111,B11111 };
byte L_T2[8] = { B00000,B00000,B10000,B11000,B11100,B11110,B11111,B11111 };
byte L_T3[8] = { B00000,B00000,B00000,B00000,B00000,B00000,B00000,B10000 };
byte L_B0[8] = { B11111,B11111,B00000,B00000,B00000,B00000,B00000,B00000 };
byte L_B1[8] = { B11111,B11111,B00000,B00000,B00000,B00000,B00000,B00000 };
byte L_B2[8] = { B11111,B11111,B11110,B11100,B11000,B10000,B00000,B00000 };
byte L_B3[8] = { B10000,B00000,B00000,B00000,B00000,B00000,B00000,B00000 };
void loadRightArrowChars() {
if (!lcd) return;
lcd->createChar(0, R_T0); lcd->createChar(1, R_T1); lcd->createChar(2, R_T2); lcd->createChar(3, R_T3);
lcd->createChar(4, R_B0); lcd->createChar(5, R_B1); lcd->createChar(6, R_B2); lcd->createChar(7, R_B3);
}
void loadLeftArrowChars() {
if (!lcd) return;
lcd->createChar(0, L_T0); lcd->createChar(1, L_T1); lcd->createChar(2, L_T2); lcd->createChar(3, L_T3);
lcd->createChar(4, L_B0); lcd->createChar(5, L_B1); lcd->createChar(6, L_B2); lcd->createChar(7, L_B3);
}
void drawHugeArrow4(bool on, int col) {
if (!lcd) return;
if (!on) {
for (int i = 0; i < 4; i++) {
lcd->setCursor(col + i, 0); lcd->print(" ");
lcd->setCursor(col + i, 1); lcd->print(" ");
}
return;
}
lcd->setCursor(col + 0, 0); lcd->write((uint8_t)0);
lcd->setCursor(col + 1, 0); lcd->write((uint8_t)1);
lcd->setCursor(col + 2, 0); lcd->write((uint8_t)2);
lcd->setCursor(col + 3, 0); lcd->write((uint8_t)3);
lcd->setCursor(col + 0, 1); lcd->write((uint8_t)4);
lcd->setCursor(col + 1, 1); lcd->write((uint8_t)5);
lcd->setCursor(col + 2, 1); lcd->write((uint8_t)6);
lcd->setCursor(col + 3, 1); lcd->write((uint8_t)7);
}
// -------------------- SLEEP ALERT ENGINE --------------------
SleepAlertMode chooseAlertByPriority() {
if (digitalRead(PIN_IN_D11) == HIGH) return ALERT_D11_OBSTACLES;
if (digitalRead(PIN_IN_D10) == HIGH) return ALERT_D10_REVERSING;
if (digitalRead(PIN_IN_D9) == HIGH) return ALERT_D9_RIGHT;
if (digitalRead(PIN_IN_D6) == HIGH) return ALERT_D6_LEFT;
return ALERT_NONE;
}
void showAlertText(SleepAlertMode m, bool on) {
if (!lcd) return;
if (m == ALERT_D10_REVERSING) {
lcd->setCursor(0, 0);
if (on) printCentered16("REVERSING!!!");
else lcd->print(" ");
lcd->setCursor(0, 1);
if (!on) printCentered16("REVERSING!!!");
else lcd->print(" ");
return;
}
if (m == ALERT_D11_OBSTACLES) {
lcd->setCursor(0, 0);
printCentered16("OBSTACLE");
lcd->setCursor(0, 1);
if (on) printCentered16("DETECTED!!!");
else lcd->print(" ");
return;
}
if (m == ALERT_D9_RIGHT) {
loadRightArrowChars();
drawHugeArrow4(on, 0);
printRightAligned12_fromCol4(0, "TURNING TO");
printRightAligned12_fromCol4(1, "THE RIGHT");
return;
}
if (m == ALERT_D6_LEFT) {
loadLeftArrowChars();
printLeftAligned12(0, "TURNING TO");
printLeftAligned12(1, "THE LEFT");
drawHugeArrow4(on, 12);
return;
}
lcd->setCursor(0, 0); lcd->print(" ");
lcd->setCursor(0, 1); lcd->print(" ");
}
void stopAlertAndReturnSleepIfNeeded() {
alertMode = ALERT_NONE;
if (anyButtonPressed()) { wakeDisplay(); return; }
if (alertWasSleeping) forceSleepNow();
alertWasSleeping = false;
}
// ✅ UPDATED: alerts now work even when buzzerEnabled==true, and we add an entry-beep
void updateSleepAlerts() {
if (!active) return;
// Only run when sleeping OR already alerting
if (!displayOff && alertMode == ALERT_NONE) return;
unsigned long now = millis();
// If user presses any button, stop alerts and restore sleep/awake properly
if (alertMode != ALERT_NONE && anyButtonPressed()) {
alertWasSleeping = false;
stopAlertAndReturnSleepIfNeeded();
return;
}
SleepAlertMode desired = chooseAlertByPriority();
// ---------------- ENTER ALERT (from sleep) ----------------
if (alertMode == ALERT_NONE) {
if (!displayOff) return; // only pop alerts from sleep mode
if (desired == ALERT_NONE) return; // nothing to show
alertWasSleeping = true;
wakeDisplay();
lcd->clear();
alertMode = desired;
alertFlashOn = true;
alertLastFlashMs = now;
showAlertText(alertMode, alertFlashOn);
// ✅ NEW: when buzzer is enabled, beep once when alert screen appears
if (buzzerEnabled && !buzzerActive && sleepAlertEntryBeepEnabled) {
if (now - lastSleepAlertBeepMs >= sleepAlertEntryBeepCooldownMs) {
lastSleepAlertBeepMs = now;
startD5OneShot(now);
}
}
return;
}
// ---------------- EXIT ALERT ----------------
if (desired == ALERT_NONE) {
stopAlertAndReturnSleepIfNeeded();
return;
}
// ---------------- SWITCH ALERT TYPE ----------------
if (desired != alertMode) {
alertMode = desired;
lcd->clear();
alertFlashOn = true;
alertLastFlashMs = now;
showAlertText(alertMode, alertFlashOn);
// ✅ optional: beep once when changing alert type
if (buzzerEnabled && !buzzerActive && sleepAlertEntryBeepEnabled) {
if (now - lastSleepAlertBeepMs >= sleepAlertEntryBeepCooldownMs) {
lastSleepAlertBeepMs = now;
startD5OneShot(now);
}
}
return;
}
// ---------------- FLASH ANIMATION ----------------
if (now - alertLastFlashMs >= ALERT_FLASH_MS) {
alertLastFlashMs = now;
alertFlashOn = !alertFlashOn;
showAlertText(alertMode, alertFlashOn);
}
}
// -------------------- Power Control --------------------
void activateSystem() {
// ✅ Do not allow activation if emergency is LOW
if (!emergencyOkNow()) return;
active = true;
digitalWrite(PIN_OUT, HIGH);
activatedAtMs = millis();
timerSeconds = 0;
lastActivityMs = activatedAtMs;
lcd->init();
lcd->clear();
rampFlickerBacklight(true);
showingReady = true;
showReadyMessageInit();
playBuzzerPattern(BUZZ_STARTUP);
}
void deactivateSystem() {
playBuzzerPattern(BUZZ_SHUTDOWN);
active = false;
digitalWrite(PIN_OUT, LOW);
a2AudioControlMode = false;
a2AudioHeaderPending = false;
a2SuppressBeepUntilRelease = false;
digitalWrite(PIN_AUDIO_D12, LOW);
digitalWrite(PIN_AUDIO_D4, LOW);
digitalWrite(PIN_AUDIO_D2, LOW);
buzzerEnabled = false;
applyBuzzerEnablePin();
buzzerSoundOff();
alertMode = ALERT_NONE;
alertWasSleeping = false;
rampFlickerBacklight(false);
lcd->clear();
}
// -------------------- Button Handlers --------------------
void handleLongPressActivation() {
unsigned long now = millis();
bool both = (digitalRead(PIN_A0) == LOW) && (digitalRead(PIN_A1) == LOW);
if (both) {
if (!bothPreviouslyPressed) {
bothPressedStart = now;
bothPreviouslyPressed = true;
} else if (now - bothPressedStart >= longPressMs) {
// ✅ Block activation if emergency is LOW
if (!active && !emergencyOkNow()) {
while (digitalRead(PIN_A0) == LOW || digitalRead(PIN_A1) == LOW) delay(20);
bothPreviouslyPressed = false;
return;
}
active ? deactivateSystem() : activateSystem();
while (digitalRead(PIN_A0) == LOW || digitalRead(PIN_A1) == LOW) delay(20);
bothPreviouslyPressed = false;
}
} else {
bothPreviouslyPressed = false;
}
}
// ✅ A2: speed cycle + audio mode
void handleA2Button() {
if (!active) return;
unsigned long now = millis();
int a2 = digitalRead(PIN_A2);
if (a2 != lastA2Read) lastA2ChangeMs = now;
if ((now - lastA2ChangeMs) > DEBOUNCE_MS && a2 != lastA2StableState) {
lastA2StableState = a2;
if (a2 == LOW) {
a2PressStartMs = now;
a2LongPressHandled = false;
if (a2AudioControlMode && buzzerEnabled) {
startD5OneShot(now);
audioLevel = (audioLevel + 1) % 3;
applyAudioOutputs(audioLevel);
showAudioModeLevelMessage(audioLevel);
lastActivityMs = now;
if (displayOff) wakeDisplay();
}
}
if (a2 == HIGH) {
unsigned long pressDur = now - a2PressStartMs;
if (!a2AudioControlMode && !a2LongPressHandled) {
if (pressDur >= 40 && pressDur < a2LongPressMs) {
if (alertMode != ALERT_NONE) { alertWasSleeping = false; stopAlertAndReturnSleepIfNeeded(); }
scrollSpeedIndex = (scrollSpeedIndex + 1) % 6;
scrollIntervalMs = scrollSpeedsMs[scrollSpeedIndex];
speedDisplayUntilMs = now + SPD_DISPLAY_MS;
lastActivityMs = now;
if (displayOff) wakeDisplay();
}
}
if (a2SuppressBeepUntilRelease) a2SuppressBeepUntilRelease = false;
}
}
if (lastA2StableState == LOW && !a2LongPressHandled) {
if ((now - a2PressStartMs) >= a2LongPressMs) {
if (!buzzerEnabled) {
a2LongPressHandled = true;
lastA2Read = a2;
return;
}
if (alertMode != ALERT_NONE) { alertWasSleeping = false; stopAlertAndReturnSleepIfNeeded(); }
a2SuppressBeepUntilRelease = true;
if (!a2AudioControlMode) {
a2AudioControlMode = true;
// ✅ FORCE INITIAL LEVEL = LEVEL 3 every activation
audioLevel = 2;
savedScrollSpeedIndex = scrollSpeedIndex;
scrollSpeedIndex = 5;
scrollIntervalMs = scrollSpeedsMs[5];
speedDisplayUntilMs = 0;
applyAudioOutputs(audioLevel);
playBuzzerPattern(BUZZ_A2_AUDIO_ON);
showAudioModeActivatedHeader(audioLevel);
} else {
a2AudioControlMode = false;
a2AudioHeaderPending = false;
scrollSpeedIndex = savedScrollSpeedIndex;
scrollIntervalMs = scrollSpeedsMs[scrollSpeedIndex];
digitalWrite(PIN_AUDIO_D12, LOW);
digitalWrite(PIN_AUDIO_D4, LOW);
digitalWrite(PIN_AUDIO_D2, LOW);
playBuzzerPattern(BUZZ_A2_AUDIO_OFF);
showAudioModeDeactivatedMessage();
}
a2LongPressHandled = true;
lastActivityMs = now;
if (displayOff) wakeDisplay();
}
}
lastA2Read = a2;
}
// ✅ A3: PRESS ONLY beep + short press changes tone stage + long press toggles buzzer on/off
void handleA3BuzzerButton() {
if (!active) return;
unsigned long now = millis();
int a3 = digitalRead(PIN_A3);
if (a3 != lastA3Read) lastA3ChangeMs = now;
if ((now - lastA3ChangeMs) > DEBOUNCE_MS && a3 != lastA3StableState) {
if (a3 == LOW) {
a3PressStartMs = now;
a3LongPressHandled = false;
if (buzzerEnabled && !buzzerActive) startD5OneShot(now);
}
if (a3 == HIGH) {
unsigned long pressDur = now - a3PressStartMs;
if (!a3LongPressHandled && pressDur >= 40 && pressDur < BUZZER_LONGPRESS_MS) {
if (buzzerEnabled) {
if (alertMode != ALERT_NONE) { alertWasSleeping = false; stopAlertAndReturnSleepIfNeeded(); }
toneStage = (ToneStage)(((int)toneStage + 1) % 3);
showToneStageMessage();
}
}
}
lastA3StableState = a3;
}
if (lastA3StableState == LOW && !a3LongPressHandled &&
(now - a3PressStartMs) >= BUZZER_LONGPRESS_MS) {
if (alertMode != ALERT_NONE) { alertWasSleeping = false; stopAlertAndReturnSleepIfNeeded(); }
buzzerEnabled = !buzzerEnabled;
applyBuzzerEnablePin();
if (!buzzerEnabled && a2AudioControlMode) {
a2AudioControlMode = false;
a2AudioHeaderPending = false;
a2SuppressBeepUntilRelease = true;
scrollSpeedIndex = savedScrollSpeedIndex;
scrollIntervalMs = scrollSpeedsMs[scrollSpeedIndex];
digitalWrite(PIN_AUDIO_D12, LOW);
digitalWrite(PIN_AUDIO_D4, LOW);
digitalWrite(PIN_AUDIO_D2, LOW);
}
capturePrevDisplayForRestore();
if (displayOff) wakeDisplay();
lcd->clear();
lcd->setCursor(0,0);
lcd->print(buzzerEnabled ? "BUZZER IS ENABLED " : "BUZZER DISABLED");
buzzerMsgActive = true;
buzzerMsgUntilMs = now + BUZZER_MSG_MS;
a3LongPressHandled = true;
lastActivityMs = now;
playBuzzerPattern(buzzerEnabled ? BUZZ_ENABLE : BUZZ_DISABLE);
}
lastA3Read = a3;
}
// -------------------- Updates --------------------
void updateBlink() {
if (millis() - lastBlinkMs >= BLINK_INTERVAL_MS) {
blinkState = !blinkState;
lastBlinkMs = millis();
}
}
void updateTimerAndScroll() {
unsigned long now = millis();
if (active) timerSeconds = (now - activatedAtMs) / 1000UL;
if (!active) return;
if (!lcd) return;
if (alertMode != ALERT_NONE) return;
if (buzzerMsgActive) {
if (now >= buzzerMsgUntilMs) {
if (a2AudioHeaderPending) {
a2AudioHeaderPending = false;
showAudioModeLevelMessage(a2AudioPendingLevel);
return;
}
buzzerMsgActive = false;
lcd->clear();
lcd->setCursor(0,0); lcd->print(prevLine0);
lcd->setCursor(0,1); lcd->print(prevLine1);
}
return;
}
if (showingReady) {
updateReadyScrolling();
updateReadyDots();
if (now - activatedAtMs >= READY_SHOW_MS) {
showingReady = false;
lcd->clear();
}
return;
}
if (!displayOff && now - lastActivityMs >= AUTO_DIM_IDLE_MS) {
rampFlickerBacklight(false);
lcd->clear();
displayOff = true;
return;
}
updateBlink();
if (a2AudioControlMode) {
scrollSpeedIndex = 5;
scrollIntervalMs = scrollSpeedsMs[5];
}
if (!displayOff) {
if (scrollSpeedIndex == 5) {
lcd->setCursor(0,0);
lcd->print(staticName16);
} else if (scrollIntervalMs > 0 && now - lastScrollMs >= scrollIntervalMs) {
scrollIndex = (scrollIndex + 1) % scrollLen;
lastScrollMs = now;
showScrollingName();
}
showTimerOrSpd(timerSeconds, speedDisplayUntilMs > now, scrollSpeedIndex);
}
}
// -------------------- EEPROM LOAD --------------------
void loadLongPressFromEEPROM() {
unsigned long v = 0;
EEPROM.get(EEPROM_LONGPRESS_ADDR, v);
if (v < 1000UL || v > 15000UL) v = DEFAULT_LONGPRESS_MS;
longPressMs = v;
}
// -------------------- Setup & Loop --------------------
void setup() {
Serial.begin(115200);
// External pull-ups on A0-A3
pinMode(PIN_A0, INPUT);
pinMode(PIN_A1, INPUT);
pinMode(PIN_A2, INPUT);
pinMode(PIN_A3, INPUT);
// ✅ A6 emergency input (analog-only pin; no internal pullup available on A6)
pinMode(PIN_EMERGENCY_A6, INPUT);
pinMode(PIN_OUT, OUTPUT);
digitalWrite(PIN_OUT, LOW);
// buzzer pins
pinMode(PIN_BUZZER_EN, OUTPUT);
digitalWrite(PIN_BUZZER_EN, LOW);
buzzerEnabled = false;
noTone(PIN_BUZZER_TONE);
// input triggers
pinMode(PIN_IN_D11, INPUT);
pinMode(PIN_IN_D10, INPUT);
pinMode(PIN_IN_D9, INPUT);
pinMode(PIN_IN_D6, INPUT);
pinMode(PIN_IN_D7, INPUT);
pinMode(PIN_IN_D8, INPUT);
// audio outputs
pinMode(PIN_AUDIO_D12, OUTPUT);
pinMode(PIN_AUDIO_D4, OUTPUT);
pinMode(PIN_AUDIO_D2, OUTPUT);
digitalWrite(PIN_AUDIO_D12, LOW);
digitalWrite(PIN_AUDIO_D4, LOW);
digitalWrite(PIN_AUDIO_D2, LOW);
for (uint8_t i = 0; i < A_COUNT; i++) {
aLastRaw[i] = digitalRead(A_PINS[i]);
aArmed[i] = true;
aLastTrigMs[i] = 0;
}
d9d6LastRaw = anyD9D6High();
d9d6Armed = !d9d6LastRaw;
loadLongPressFromEEPROM();
Wire.begin();
lcd = new LiquidCrystal_I2C(0x27, 16, 2);
lcd->init();
lcd->noBacklight();
// Initialize emergency state
emergencyOkStable = emergencyOkNow();
emergencyLowSinceMs = 0;
// If emergency is already LOW at boot, ensure system stays OFF
if (!emergencyOkStable) {
digitalWrite(PIN_OUT, LOW);
digitalWrite(PIN_BUZZER_EN, LOW);
buzzerSoundOff();
}
}
void loop() {
// ✅ MUST be first: emergency shutdown overrides everything
updateEmergencyShutdown();
if (!emergencyOkStable) return; // remain locked-out until A6 returns HIGH
updateD7D8RobotTones();
updateBuzzer();
updateD5Output();
updateSleepAlerts();
handleLongPressActivation();
handleA2Button();
handleA3BuzzerButton();
updateTimerAndScroll();
if (active && displayOff && anyButtonPressed())
wakeDisplay();
}