// ============================================================================
// CREATED AND DESIGNED BY: MARVIN A. QUIZZAGAN - LCD DISPLAY WITH 4x4 SWITCH MATRIX
// DATE: MARCH 14, 2026
// FINAL STABLE VERSION - D13 / D1 FUNCTIONS SWAPPED
//
// IMPORTANT NOTE:
// PIN D13 CANNOT BE USED FOR 4x4 MATRIX KEYPAD DUE TO ONBOARD LED / RESISTOR
//
// NANO (2KB SRAM) STABILITY PATCH:
// - Removed large SRAM hogs
// - Favorites list is virtual (no giant concatenated string)
// - Uses one global scroll buffer
// - Uses snprintf_P + PSTR
// - Many lcd.print literals wrapped with F()
//
// D13 / D1 SWAP:
// - D1 is now MP3 OUTPUT bit0 (replaces old D13 output function)
// - D13 is now keypad indicator LED output
//
// D13 INDICATOR:
// - DEFAULT = LOW
// - ANY KEYPAD PRESS = HIGH while held
// - KEY RELEASE = LOW
//
// A6 UPDATE:
// - A6 acts like keypad "A"
// - CLASSIC NANO A6 is analog-only, so USE EXTERNAL PULL-UP TO +5V
// - A6 LOW = virtual A press
// - A6 HIGH = virtual A release
// - Uses smooth hysteresis + debounce
// - Keypad A and A6 share the SAME helper path
//
// STARTUP MP3 UPDATE:
// - At power-up, D1 goes LOW for 1 second to play startup MP3
// - Then outputs reset to OFF
// - Then normal operation begins
//
// A6 WIRING:
// +5V ---[10k]--- A6 --- push button --- GND
// ============================================================================
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Keypad.h>
#include <EEPROM.h>
#include <stdio.h>
#include <string.h>
#include <avr/pgmspace.h>
// ---------------- LCD ----------------
LiquidCrystal_I2C lcd(0x27, 16, 2);
// -------------- Keypad 4x4 ----------
const byte ROWS = 4;
const byte COLS = 4;
char keys[ROWS][COLS] = {
{'1','2','3','A'},
{'4','5','6','B'},
{'7','8','9','C'},
{'*','0','#','D'}
};
byte rowPins[ROWS] = {4, 3, 2, A3}; // {A2, A1, A0, 12}; //
byte colPins[COLS] = {A2, A1, A0, 12}; // {4, 3, 2, A3}; //
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
// ----------- 8-bit Output Pins -------
// D5 = bit7 ... D1 = bit0 (INVERTED LOGIC)
// D1 replaced old D13 output function
const byte outPins[8] = {5, 6, 7, 8, 9, 10, 11, 1};
// ----------- D0 INPUT (trigger) -------
const byte TRIG_PIN = 0; // D0 (RX)
// ----------- D13 INDICATOR LED OUTPUT ------
const byte KEYPULSE_PIN = 13; // swapped from D1 to D13
// ----------- A6 VIRTUAL "A" INPUT ---------
const byte A6_VIRTUAL_A_PIN = A6;
// ---------------- STARTUP / ANTI-GLITCH ----------------
const unsigned long STARTUP_LOCKOUT_MS = 250;
const unsigned long STARTUP_MP3_PULSE_MS = 1000;
const uint16_t STARTUP_MP3_VALUE = 1; // bit0 = D1 LOW only (inverted outputs)
bool startupReady = false;
bool startupPulseDone = false;
unsigned long startupMs = 0;
unsigned long startupPulseStartMs = 0;
unsigned long nowMs = 0;
// ---------------- OUTPUT SHADOWS ----------------
uint16_t activeOutputValue = 0xFFFF; // force first update
bool keyPulseState = false;
// ---------------- D0 TRIGGER FILTER ----------------
const unsigned long TRIG_DEBOUNCE_MS = 30;
struct StableDigitalIn {
bool rawState;
bool stableState;
bool prevStableState;
unsigned long lastEdgeMs;
};
StableDigitalIn trigFilter;
// ----------- A6 thresholds / debounce ----------
const int A6_PRESS_THRESHOLD = 120;
const int A6_RELEASE_THRESHOLD = 700;
const unsigned long A6_DEBOUNCE_MS = 25;
bool a6RawPressed = false;
bool a6RawPrev = false;
bool a6StablePressed = false;
unsigned long a6LastEdgeMs = 0;
// -------- OUTPUT PULSE TIMER ----------
const unsigned long OUTPUT_PULSE_MS = 2000;
unsigned long pulseUntilMs = 0;
// -------- LONG PRESS TUNABLES ----------
unsigned long HASH_HOLD_MS = 5000;
unsigned long A_HOLD_MS = 5000;
unsigned long B_HOLD_MS = 2000;
unsigned long C_HOLD_MS = 2000;
unsigned long D_HOLD_MS = 2500;
unsigned long STAR_HOLD_MS = 5000;
// -------- ALERT SCREEN (generic) ------
unsigned long ALERT_SHOW_MS_DEFAULT = 5000;
unsigned long ALERT_BLINK_MS_DEFAULT = 400;
bool alertActive = false;
unsigned long alertUntilMs = 0;
unsigned long lastAlertBlinkMs = 0;
bool alertBlinkState = true;
unsigned long alertBlinkMs = 400;
char alertLine1[17];
char alertLine2[17];
// -------- CURSOR BLINK (typing UI) ----
bool cursorBlinkState = true;
unsigned long lastCursorBlinkMs = 0;
const unsigned long CURSOR_BLINK_MS = 500;
// -------- LINE 1 BLINK (NORMAL SCREEN: 0-255 and #) ----
bool line1BlinkState = true;
unsigned long lastLine1BlinkMs = 0;
unsigned long LINE1_BLINK_MS = 500;
bool LINE1_BLINK_ENABLE = true;
// -------- FAVORITES MODE BLINK ----
bool favBlinkState = true;
unsigned long lastFavBlinkMs = 0;
unsigned long FAV_BLINK_MS = 600;
bool favCursorBlinkState = true;
unsigned long lastFavCursorBlinkMs = 0;
// only "(B)" blink
unsigned long FAV_B_BLINK_MS = 450;
bool favBBlinkState = true;
unsigned long lastFavBBlinkMs = 0;
// ------------- Input buffer -----------
char buf[4] = {'\0','\0','\0','\0'};
byte bufLen = 0;
bool entryDirty = false;
// ----------------- UI timing ----------
const unsigned long TYPING_HOLD_MS = 5000;
const unsigned long APPLIED_SHOW_MS = 1000;
const unsigned long ERROR_SHOW_MS = 3000;
const unsigned long SCROLL_STEP_MS = 250;
const unsigned long ERROR_SCROLL_STEP_MS = 220;
unsigned long typingUntilMs = 0;
unsigned long appliedUntilMs = 0;
unsigned long errorUntilMs = 0;
unsigned long lastScrollMs = 0;
int scrollPos = 0;
int errorScrollPos = 0;
// Playing latch
uint16_t playingValue = 0;
uint16_t lastPlayedValue = 0;
uint16_t lastErrorValue = 0;
// After LONG * => forget previous number behavior
bool forgetPrev = false;
// ----------- Modes -----------
enum Mode : byte {
MODE_NORMAL,
MODE_REPEAT,
MODE_STEP,
MODE_FAV_EDIT,
MODE_FAV_PLAY,
MODE_FAV_ERASE_AUTH,
MODE_FAV_LIST_EDIT_AUTH,
MODE_FAV_LIST_EDIT,
MODE_FAV_ITEM_EDIT
};
Mode mode = MODE_NORMAL;
// STEP mode state
uint16_t stepCurrent = 0;
// -------- Long-press tracking --------
bool hashDown = false; unsigned long hashDownStartMs = 0; bool hashLongFired = false;
bool aDown = false; unsigned long aDownStartMs = 0; bool aLongFired = false;
bool bDown = false; unsigned long bDownStartMs = 0; bool bLongFired = false;
bool cDown = false; unsigned long cDownStartMs = 0; bool cLongFired = false;
bool dDown = false; unsigned long dDownStartMs = 0; bool dLongFired = false;
bool starDown = false; unsigned long starDownStartMs = 0; bool starLongFired = false;
// -------------------- FAVORITES DATA --------------------
const byte MAX_FAV = 10;
uint16_t favList[MAX_FAV];
byte favCount = 0;
byte favIndex = 0;
unsigned long favMsgUntilMs = 0;
char favLastLine2[17] = "________________";
bool favTypingView = false;
const byte FAV_SAVE_COL = 16 - (sizeof("SAVE(B)") - 1);
// -------------------- EEPROM PERSISTENCE --------------------
const int EEPROM_ADDR = 0;
const uint16_t EEPROM_MAGIC = 0x4D51;
const uint8_t EEPROM_VER = 1;
struct FavEepromBlob {
uint16_t magic;
uint8_t ver;
uint8_t count;
uint16_t list[MAX_FAV];
uint16_t checksum;
};
// -------------------- PASSWORD --------------------
const char PW_CODE[] = "161979";
const byte PW_LEN = 6;
char pwBuf[PW_LEN + 1];
byte pwLen = 0;
unsigned long pwWrongFlag = 0; // 0=none, 1=return to D auth, 2=return to C auth
// ---- PASSWORD ENTRY TIMEOUT ----
const unsigned long PW_IDLE_TIMEOUT_MS = 5000;
unsigned long pwLastInputMs = 0;
// ---- RANDOM MASK DISPLAY ----
char pwMask[PW_LEN + 1];
// ---- SMOOTH CURSOR BLINK (D ERASE AUTH) ----
bool pwCursorBlinkState = true;
unsigned long lastPwCursorBlinkMs = 0;
const unsigned long PW_CURSOR_BLINK_MS = 450;
// ---- SMOOTH CURSOR BLINK (C EDIT AUTH) ----
bool pwEditCursorBlinkState = true;
unsigned long lastPwEditCursorBlinkMs = 0;
const unsigned long PW_EDIT_CURSOR_BLINK_MS = PW_CURSOR_BLINK_MS;
// -------------------- LIST EDIT STATE --------------------
byte editSelIndex = 0;
int editScrollPos = 0;
unsigned long lastEditScrollMs = 0;
char editWin[17];
// =========================================================
// BACKLIGHT + DISPLAY SLEEP (TUNABLE)
// =========================================================
unsigned long LCD_IDLE_BEFORE_DIM_MS = 180000;
unsigned long LCD_DIM_DURATION_MS = 3000;
const unsigned long DIM_PWM_PERIOD_MS = 5;
unsigned long lastDimPwmMs = 0;
byte dimPwmPhase = 0;
unsigned long lastKeyTouchMs = 0;
bool dimmingActive = false;
unsigned long dimStartMs = 0;
bool displaySleeping = false;
// Freeze LCD writes during dim/sleep
bool lcdWritesFrozen = false;
// =========================================================
// WRONG PASSWORD ALERT (SOUND DISABLED - D13 IS LED NOW)
// =========================================================
unsigned long PW_WRONG_TOTAL_MS = 1800;
unsigned long PW_WRONG_FLASH_MS = 220;
unsigned int PW_SIREN_MIN_HZ = 650;
unsigned int PW_SIREN_MAX_HZ = 1800;
unsigned int PW_SIREN_STEP_HZ = 60;
unsigned long PW_SIREN_ON_MS = 90;
unsigned long PW_SIREN_OFF_MS = 70;
bool PW_SIREN_ENABLE = false;
bool alarmToneActive = false;
unsigned long alarmUntilMs = 0;
unsigned long alarmNextToggleMs = 0;
bool alarmOn = false;
int alarmDir = +1;
unsigned int alarmFreqHz = 900;
// =========================================================
// SUCCESS TONE (DISABLED - D13 IS LED NOW)
// =========================================================
bool SUCCESS_TONE_ENABLE = false;
unsigned int SUCCESS_TONE_HZ_1 = 1200;
unsigned int SUCCESS_TONE_HZ_2 = 1800;
unsigned int SUCCESS_TONE_HZ_3 = 2400;
unsigned long SUCCESS_STEP_MS = 70;
unsigned long SUCCESS_GAP_MS = 25;
bool successToneActive = false;
byte successToneStep = 0;
unsigned long successNextMs = 0;
// =========================================================
// ONE GLOBAL SCROLL BUFFER
// =========================================================
char scrollBuf[96];
// -------------------- Forward Decls --------------------
void resetVisualTimers();
void enterFavListEditAuthMode();
void enterEraseAuthMode();
void favEnterEditMode();
void enterFavListEditMode();
void handleACommonPress();
void handleACommonRelease();
void handleVirtualAPress();
void handleVirtualARelease();
void updateVirtualAInput();
void initTriggerFilter();
bool updateTriggerRisingEdge();
void setKeyPulse(bool on);
void writeOutputsRaw(uint16_t value);
void setOutputsFromValue(uint16_t value);
void forceOutputsOff();
// =========================================================
// EEPROM
// =========================================================
uint16_t checksumBlob(const FavEepromBlob &b) {
uint32_t sum = 0;
sum += b.magic;
sum += b.ver;
sum += b.count;
for (byte i = 0; i < MAX_FAV; i++) sum += b.list[i];
return (uint16_t)(sum & 0xFFFF);
}
void saveFavoritesToEEPROM() {
FavEepromBlob b;
b.magic = EEPROM_MAGIC;
b.ver = EEPROM_VER;
b.count = favCount;
for (byte i = 0; i < MAX_FAV; i++) b.list[i] = (i < favCount) ? favList[i] : 0;
b.checksum = checksumBlob(b);
EEPROM.put(EEPROM_ADDR, b);
}
void loadFavoritesFromEEPROM() {
FavEepromBlob b;
EEPROM.get(EEPROM_ADDR, b);
if (b.magic != EEPROM_MAGIC || b.ver != EEPROM_VER) { favCount = 0; return; }
if (b.checksum != checksumBlob(b)) { favCount = 0; return; }
if (b.count > MAX_FAV) { favCount = 0; return; }
favCount = b.count;
for (byte i = 0; i < favCount; i++) {
uint16_t v = b.list[i];
if (v > 255) v = 0;
favList[i] = v;
}
}
// -------------------- Stable Output Helpers --------------------
void setKeyPulse(bool on) {
if (keyPulseState != on) {
keyPulseState = on;
digitalWrite(KEYPULSE_PIN, on ? HIGH : LOW);
}
}
void writeOutputsRaw(uint16_t value) {
for (byte i = 0; i < 8; i++) {
byte bitVal = (value >> (7 - i)) & 0x01;
digitalWrite(outPins[i], bitVal ? LOW : HIGH); // inverted logic
}
}
void setOutputsFromValue(uint16_t value) {
if (value == activeOutputValue) return;
activeOutputValue = value;
writeOutputsRaw(value);
}
void forceOutputsOff() {
activeOutputValue = 0xFFFF;
setOutputsFromValue(0);
}
// -------------------- Trigger Filter --------------------
void initTriggerFilter() {
bool r = digitalRead(TRIG_PIN);
trigFilter.rawState = r;
trigFilter.stableState = r;
trigFilter.prevStableState = r;
trigFilter.lastEdgeMs = millis();
}
bool updateTriggerRisingEdge() {
bool raw = digitalRead(TRIG_PIN);
if (raw != trigFilter.rawState) {
trigFilter.rawState = raw;
trigFilter.lastEdgeMs = nowMs;
}
if ((unsigned long)(nowMs - trigFilter.lastEdgeMs) >= TRIG_DEBOUNCE_MS) {
if (trigFilter.stableState != trigFilter.rawState) {
trigFilter.prevStableState = trigFilter.stableState;
trigFilter.stableState = trigFilter.rawState;
if (trigFilter.prevStableState == LOW && trigFilter.stableState == HIGH) {
return true;
}
}
}
return false;
}
// -------------------- Helpers --------------------
uint16_t bufferToNumber() { return bufLen ? (uint16_t)atoi(buf) : 0; }
void clearBuffer() {
bufLen = 0;
buf[0] = buf[1] = buf[2] = '\0';
buf[3] = '\0';
entryDirty = false;
}
void appendDigit(char d) {
forgetPrev = false;
if (bufLen < 3) {
buf[bufLen++] = d;
buf[bufLen] = '\0';
} else {
buf[0] = buf[1];
buf[1] = buf[2];
buf[2] = d;
buf[3] = '\0';
bufLen = 3;
}
entryDirty = true;
}
void backspaceDigit() {
if (bufLen == 0) return;
bufLen--;
buf[bufLen] = '\0';
entryDirty = (bufLen > 0);
}
// -------------------- RANDOM MASK HELPERS --------------------
char randomDigitNot(char avoidChar) {
byte avoid = (byte)(avoidChar - '0');
byte d;
do { d = (byte)random(0, 10); } while (d == avoid);
return (char)('0' + d);
}
void regenPwMaskAll() {
for (byte i = 0; i < pwLen && i < PW_LEN; i++) {
pwMask[i] = randomDigitNot(pwBuf[i]);
}
pwMask[pwLen] = '\0';
}
// -------------------- LCD write guards --------------------
inline bool lcdCanWrite() { return !lcdWritesFrozen; }
// -------------------- LCD helpers --------------------
void clearLine2() {
if (!lcdCanWrite()) return;
lcd.setCursor(0, 1);
lcd.print(F(" "));
}
void initLine1() {
if (!lcdCanWrite()) return;
lcd.setCursor(0, 0); lcd.print(F("SEL:"));
lcd.setCursor(9, 0); lcd.print(F(",THEN "));
lcd.setCursor(4, 0); lcd.print(F("0-255"));
lcd.setCursor(15, 0); lcd.print(F("#"));
}
void showMp3PlayModeLine1() {
if (!lcdCanWrite()) return;
lcd.setCursor(0, 0);
lcd.print(F("MP3 PLAY MODE: "));
}
void updateLine1Blink() {
if (!lcdCanWrite()) return;
if (!LINE1_BLINK_ENABLE) {
lcd.setCursor(4, 0); lcd.print(F("0-255"));
lcd.setCursor(15, 0); lcd.print(F("#"));
return;
}
if (nowMs - lastLine1BlinkMs >= LINE1_BLINK_MS) {
lastLine1BlinkMs = nowMs;
line1BlinkState = !line1BlinkState;
if (line1BlinkState) {
lcd.setCursor(4, 0); lcd.print(F("0-255"));
lcd.setCursor(15, 0); lcd.print(F("#"));
} else {
lcd.setCursor(4, 0); lcd.print(F(" "));
lcd.setCursor(15, 0); lcd.print(F(" "));
}
}
}
void showTypedDigitsLine2() {
if (!lcdCanWrite()) return;
if (nowMs - lastCursorBlinkMs >= CURSOR_BLINK_MS) {
lastCursorBlinkMs = nowMs;
cursorBlinkState = !cursorBlinkState;
}
lcd.setCursor(0, 1);
lcd.print(F("eMAR TO PLAY:"));
char d0 = '_', d1 = '_', d2 = '_';
if (bufLen == 1) d2 = buf[0];
else if (bufLen == 2) { d1 = buf[0]; d2 = buf[1]; }
else if (bufLen >= 3) { d0 = buf[bufLen - 3]; d1 = buf[bufLen - 2]; d2 = buf[bufLen - 1]; }
if (!cursorBlinkState && bufLen < 3) {
if (bufLen == 0) d2 = ' ';
if (bufLen == 1) d1 = ' ';
if (bufLen == 2) d0 = ' ';
}
lcd.setCursor(13, 1);
lcd.print(d0); lcd.print(d1); lcd.print(d2);
}
void showAppliedLine2(uint16_t value) {
if (!lcdCanWrite()) return;
clearLine2();
lcd.setCursor(0, 1);
lcd.print(F("APPLIED "));
lcd.print(value);
int used = 8 + (value < 10 ? 1 : (value < 100 ? 2 : 3));
for (int i = used; i < 16; i++) lcd.print(' ');
}
// -------------------- SAFE Scroll builders --------------------
void buildNormalScrollText(char *out, size_t outSize, uint16_t value) {
if (value == 0) snprintf_P(out, outSize, PSTR("E-MAR IS READY TO PLAY NOW "));
else snprintf_P(out, outSize, PSTR("PLAYING %u "), (unsigned)value);
}
void buildRepeatScrollText(char *out, size_t outSize, uint16_t value) {
if (value == 0) snprintf_P(out, outSize, PSTR("E-MAR IS READY TO PLAY NOW "));
else snprintf_P(out, outSize, PSTR("PLAYING %u REPEATEDLY "), (unsigned)value);
}
void buildStepScrollText(char *out, size_t outSize, uint16_t value) {
snprintf_P(out, outSize, PSTR("STEP PLAY MODE: PLAYING %u "), (unsigned)value);
}
void buildErrorScrollText(char *out, size_t outSize, uint16_t value) {
snprintf_P(out, outSize, PSTR("ERROR %u OUT OF RANGE "), (unsigned)value);
}
void buildFavNowText(char *out, size_t outSize, byte idx, byte total, uint16_t v) {
snprintf_P(out, outSize, PSTR("FAV %u/%u PLAY %u "),
(unsigned)(idx + 1), (unsigned)total, (unsigned)v);
}
void showScrollWindow(const char *text, int &pos) {
if (!lcdCanWrite()) return;
int len = (int)strlen(text);
if (len <= 0) return;
if (pos >= len) pos = 0;
char win[17];
for (byte i = 0; i < 16; i++) win[i] = text[(pos + i) % len];
win[16] = '\0';
lcd.setCursor(0, 1);
lcd.print(win);
}
// -------------------- Visual reset --------------------
void resetVisualTimers() {
typingUntilMs = 0;
appliedUntilMs = 0;
errorUntilMs = 0;
favMsgUntilMs = 0;
scrollPos = 0;
errorScrollPos = 0;
lastScrollMs = nowMs;
clearLine2();
}
// -------------------- ALERT --------------------
void startAlertScreen(const char *l1, const char *l2, unsigned long showMs, unsigned long blinkMs) {
alertActive = true;
alertUntilMs = nowMs + showMs;
lastAlertBlinkMs = 0;
alertBlinkState = true;
alertBlinkMs = blinkMs;
snprintf(alertLine1, sizeof(alertLine1), "%-16.16s", l1);
snprintf(alertLine2, sizeof(alertLine2), "%-16.16s", l2);
if (!lcdCanWrite()) return;
lcd.setCursor(0, 0); lcd.print(alertLine1);
lcd.setCursor(0, 1); lcd.print(alertLine2);
}
void updateAlertScreen() {
if (!alertActive) return;
if (nowMs >= alertUntilMs) {
alertActive = false;
return;
}
if (!lcdCanWrite()) return;
if (nowMs - lastAlertBlinkMs >= alertBlinkMs) {
lastAlertBlinkMs = nowMs;
alertBlinkState = !alertBlinkState;
if (alertBlinkState) {
lcd.setCursor(0, 0); lcd.print(alertLine1);
lcd.setCursor(0, 1); lcd.print(alertLine2);
} else {
lcd.setCursor(0, 0); lcd.print(F(" "));
lcd.setCursor(0, 1); lcd.print(F(" "));
}
}
}
// =========================================================
// D13 ALARM / SUCCESS FUNCTIONS DISABLED
// =========================================================
void stopAlarmTone() {
alarmToneActive = false;
alarmOn = false;
setKeyPulse(false);
}
void startAlarmTone(unsigned long totalMs) {
alarmToneActive = false;
(void)totalMs;
}
void updateAlarmTone() {
}
void startSuccessTone() {
successToneActive = false;
}
void updateSuccessTone() {
}
void startWrongCodeAlert(byte returnFlag) {
pwWrongFlag = returnFlag;
startAlertScreen("WRONG CODE", "TRY AGAIN", PW_WRONG_TOTAL_MS, PW_WRONG_FLASH_MS);
startAlarmTone(PW_WRONG_TOTAL_MS);
}
void triggerKeyPulse() {
}
// =========================================================
// VIRTUAL FAVORITES LIST
// =========================================================
int favVirtualLen() {
if (favCount == 0) return 20;
int len = 0;
for (byte i = 0; i < favCount; i++) {
byte idx = i + 1;
len += 2;
len += (idx < 10 ? 1 : 2);
len += 3;
if (i != favCount - 1) len += 2;
}
len += 16;
return len;
}
char favCharAt(int p) {
if (favCount == 0) {
static const char emptyMsg[] = "EMPTY ";
return emptyMsg[p % (int)(sizeof(emptyMsg) - 1)];
}
for (byte i = 0; i < favCount; i++) {
char seg[10];
byte idx = i + 1;
uint16_t v = favList[i];
snprintf(seg, sizeof(seg), "(%u)%03u", idx, (unsigned)v);
int segLen = (int)strlen(seg);
int totalSeg = segLen + ((i != favCount - 1) ? 2 : 0);
if (p < totalSeg) {
if (p < segLen) return seg[p];
return (p == segLen) ? ',' : ' ';
}
p -= totalSeg;
}
return ' ';
}
static void printPad16(const char *s) {
if (!lcdCanWrite()) return;
lcd.print(s);
int n = (int)strlen(s);
for (int i = n; i < 16; i++) lcd.print(' ');
}
// -------------------- RESET --------------------
void fullResetToNormal() {
mode = MODE_NORMAL;
pulseUntilMs = 0;
forceOutputsOff();
clearBuffer();
playingValue = 0;
stepCurrent = 0;
favIndex = 0;
pwLen = 0;
pwBuf[0] = '\0';
pwMask[0] = '\0';
pwWrongFlag = 0;
pwLastInputMs = 0;
pwCursorBlinkState = true;
lastPwCursorBlinkMs = nowMs;
pwEditCursorBlinkState = true;
lastPwEditCursorBlinkMs = nowMs;
editSelIndex = 0;
editScrollPos = 0;
hashDown = false; hashLongFired = false;
aDown = false; aLongFired = false;
bDown = false; bLongFired = false;
cDown = false; cLongFired = false;
dDown = false; dLongFired = false;
starDown = false; starLongFired = false;
a6RawPressed = false;
a6RawPrev = false;
a6StablePressed = false;
stopAlarmTone();
successToneActive = false;
setKeyPulse(false);
initLine1();
resetVisualTimers();
}
// -------------------- LONG * behavior --------------------
void longStarForgetReset() {
forgetPrev = true;
lastPlayedValue = 0;
fullResetToNormal();
startAlertScreen("ATTENTION: ALL", "CLEARED & RESET",
ALERT_SHOW_MS_DEFAULT, ALERT_BLINK_MS_DEFAULT);
}
// -------------------- Play helpers --------------------
void startPulsePlay(uint16_t v) {
mode = MODE_NORMAL;
forgetPrev = false;
playingValue = v;
lastPlayedValue = v;
setOutputsFromValue(v);
pulseUntilMs = nowMs + OUTPUT_PULSE_MS;
appliedUntilMs = nowMs + APPLIED_SHOW_MS;
showAppliedLine2(v);
clearBuffer();
typingUntilMs = 0;
scrollPos = 0;
lastScrollMs = nowMs;
}
void enterRepeatMode(uint16_t v) {
if (mode == MODE_STEP) return;
mode = MODE_REPEAT;
pulseUntilMs = 0;
playingValue = v;
lastPlayedValue = v;
setOutputsFromValue(v);
clearBuffer();
typingUntilMs = 0;
appliedUntilMs = 0;
errorUntilMs = 0;
clearLine2();
scrollPos = 0;
lastScrollMs = nowMs;
}
void enterStepMode(uint16_t startVal) {
mode = MODE_STEP;
showMp3PlayModeLine1();
pulseUntilMs = 0;
forceOutputsOff();
if (startVal > 255) startVal = 255;
stepCurrent = startVal;
playingValue = stepCurrent;
lastPlayedValue = stepCurrent;
setOutputsFromValue(stepCurrent);
pulseUntilMs = nowMs + OUTPUT_PULSE_MS;
clearBuffer();
typingUntilMs = 0;
appliedUntilMs = nowMs + APPLIED_SHOW_MS;
showAppliedLine2(stepCurrent);
clearLine2();
scrollPos = 0;
lastScrollMs = nowMs;
}
void stepAdvanceOnTrig() {
if (mode != MODE_STEP) return;
if (stepCurrent >= 255) {
fullResetToNormal();
return;
}
stepCurrent++;
playingValue = stepCurrent;
lastPlayedValue = stepCurrent;
setOutputsFromValue(stepCurrent);
pulseUntilMs = nowMs + OUTPUT_PULSE_MS;
appliedUntilMs = nowMs + APPLIED_SHOW_MS;
showAppliedLine2(stepCurrent);
scrollPos = 0;
lastScrollMs = nowMs;
}
// -------------------- FAVORITES helpers --------------------
bool favAdd(uint16_t v) {
if (v > 255) return false;
if (favCount >= MAX_FAV) return false;
favList[favCount++] = v;
saveFavoritesToEEPROM();
return true;
}
void favClearAll() {
favCount = 0;
favIndex = 0;
saveFavoritesToEEPROM();
}
void favDrawLine1Once() {
if (!lcdCanWrite()) return;
lcd.setCursor(0, 0);
lcd.print(F("FAVORITES MODE "));
}
void favDrawPromptOnce() {
favTypingView = false;
favDrawLine1Once();
if (!lcdCanWrite()) return;
lcd.setCursor(0, 1);
lcd.print(F("ENT:0-255,SAVE:B"));
lcd.setCursor(4, 1); lcd.print(F("0-255"));
lcd.setCursor(15, 1); lcd.print(F("B"));
strncpy(favLastLine2, "ENT:0-255,SAVE:B", 16);
favLastLine2[16] = '\0';
favCursorBlinkState = true;
lastFavCursorBlinkMs = nowMs;
favBBlinkState = true;
lastFavBBlinkMs = nowMs;
}
void favDrawTypingLine2Once() {
favTypingView = true;
if (!lcdCanWrite()) return;
char line[17];
for (byte i = 0; i < 16; i++) line[i] = ' ';
line[16] = '\0';
line[0] = 'F'; line[1] = 'A'; line[2] = 'V'; line[3] = ':';
line[4] = '_'; line[5] = '_'; line[6] = '_';
if (bufLen == 1) {
line[6] = buf[0];
} else if (bufLen == 2) {
line[5] = buf[0];
line[6] = buf[1];
} else if (bufLen >= 3) {
line[4] = buf[bufLen - 3];
line[5] = buf[bufLen - 2];
line[6] = buf[bufLen - 1];
}
line[FAV_SAVE_COL + 0] = 'S';
line[FAV_SAVE_COL + 1] = 'A';
line[FAV_SAVE_COL + 2] = 'V';
line[FAV_SAVE_COL + 3] = 'E';
line[FAV_SAVE_COL + 4] = '(';
line[FAV_SAVE_COL + 5] = 'B';
line[FAV_SAVE_COL + 6] = ')';
if (strncmp(line, favLastLine2, 16) != 0) {
favDrawLine1Once();
lcd.setCursor(0, 1);
lcd.print(line);
strncpy(favLastLine2, line, 16);
favLastLine2[16] = '\0';
favCursorBlinkState = true;
lastFavCursorBlinkMs = nowMs;
favBBlinkState = true;
lastFavBBlinkMs = nowMs;
}
}
void favBlinkPromptUpdate() {
if (!lcdCanWrite()) return;
if ((unsigned long)(nowMs - lastFavBlinkMs) < FAV_BLINK_MS) return;
lastFavBlinkMs += FAV_BLINK_MS;
favBlinkState = !favBlinkState;
if (favBlinkState) {
lcd.setCursor(4, 1); lcd.print(F("0-255"));
lcd.setCursor(15, 1); lcd.print(F("B"));
} else {
lcd.setCursor(4, 1); lcd.print(F(" "));
lcd.setCursor(15, 1); lcd.print(F(" "));
}
}
void favBlinkTypingSaveUpdate() {
if (!lcdCanWrite()) return;
if ((unsigned long)(nowMs - lastFavBBlinkMs) < FAV_B_BLINK_MS) return;
lastFavBBlinkMs += FAV_B_BLINK_MS;
favBBlinkState = !favBBlinkState;
lcd.setCursor(FAV_SAVE_COL, 1);
lcd.print(F("SAVE"));
lcd.setCursor(FAV_SAVE_COL + 4, 1);
if (favBBlinkState) lcd.print(F("(B)"));
else lcd.print(F(" "));
}
void favBlinkTypingCursorUpdate() {
if (!lcdCanWrite()) return;
if (bufLen >= 3) return;
if (nowMs - lastFavCursorBlinkMs >= CURSOR_BLINK_MS) {
lastFavCursorBlinkMs = nowMs;
favCursorBlinkState = !favCursorBlinkState;
byte cursorCol = 6 - bufLen;
lcd.setCursor(cursorCol, 1);
lcd.print(favCursorBlinkState ? "_" : " ");
}
}
void favEnterEditMode() {
mode = MODE_FAV_EDIT;
clearBuffer();
favMsgUntilMs = 0;
favBlinkState = true;
lastFavBlinkMs = nowMs;
favCursorBlinkState = true;
lastFavCursorBlinkMs = nowMs;
favBBlinkState = true;
lastFavBBlinkMs = nowMs;
favDrawPromptOnce();
}
void favEnterPlayMode() {
if (favCount == 0) {
if (lcdCanWrite()) {
lcd.setCursor(0, 0); lcd.print(F("FAVORITES MODE "));
lcd.setCursor(0, 1); lcd.print(F("EMPTY - ADD NUM "));
}
favMsgUntilMs = nowMs + 1500;
return;
}
mode = MODE_FAV_PLAY;
clearBuffer();
appliedUntilMs = 0;
errorUntilMs = 0;
typingUntilMs = 0;
favIndex = 0;
playingValue = favList[favIndex];
lastPlayedValue = playingValue;
if (lcdCanWrite()) {
lcd.setCursor(0, 0);
lcd.print(F("FAV PLAY D0/B "));
}
setOutputsFromValue(playingValue);
pulseUntilMs = nowMs + OUTPUT_PULSE_MS;
scrollPos = 0;
lastScrollMs = nowMs;
}
void favAdvance() {
if (mode != MODE_FAV_PLAY) return;
if (favCount == 0) { fullResetToNormal(); return; }
if (favIndex >= (favCount - 1)) {
fullResetToNormal();
return;
}
favIndex++;
playingValue = favList[favIndex];
lastPlayedValue = playingValue;
setOutputsFromValue(playingValue);
pulseUntilMs = nowMs + OUTPUT_PULSE_MS;
scrollPos = 0;
lastScrollMs = nowMs;
}
void favShowCountAndLast() {
favDrawLine1Once();
if (!lcdCanWrite()) return;
lcd.setCursor(0, 1);
lcd.print(F(" "));
char line[17];
if (favCount > 0) {
uint16_t last = favList[favCount - 1];
snprintf(line, sizeof(line), "CNT:%2u LAST:%03u ", (unsigned)favCount, (unsigned)last);
} else {
snprintf(line, sizeof(line), "CNT:%2u LAST:--- ", (unsigned)favCount);
}
lcd.setCursor(0, 1);
lcd.print(line);
favMsgUntilMs = nowMs + 1300;
favBlinkState = true;
lastFavBlinkMs = nowMs;
favCursorBlinkState = true;
lastFavCursorBlinkMs = nowMs;
favBBlinkState = true;
lastFavBBlinkMs = nowMs;
}
// -------------------- D erase favorites auth mode --------------------
void enterEraseAuthMode() {
mode = MODE_FAV_ERASE_AUTH;
pwLen = 0;
pwBuf[0] = '\0';
pwMask[0] = '\0';
pwWrongFlag = 0;
pwCursorBlinkState = true;
lastPwCursorBlinkMs = nowMs;
forceOutputsOff();
pulseUntilMs = 0;
if (lcdCanWrite()) {
lcd.setCursor(0, 0); lcd.print(F("ENTER THE CODE: "));
lcd.setCursor(0, 1); lcd.print(F(" "));
lcd.setCursor(0, 1); lcd.print(F("_"));
}
pwLastInputMs = nowMs;
}
void showEraseMaskedQuiet() {
if (!lcdCanWrite()) return;
lcd.setCursor(0, 1);
lcd.print(F(" "));
lcd.setCursor(0, 1);
for (byte i = 0; i < pwLen && i < PW_LEN; i++) lcd.print(pwMask[i]);
byte col = pwLen;
if (col > 15) col = 15;
lcd.setCursor(col, 1);
lcd.print(F("_"));
pwCursorBlinkState = true;
lastPwCursorBlinkMs = nowMs;
}
void erasePwCursorUpdate() {
if (!lcdCanWrite()) return;
if (mode != MODE_FAV_ERASE_AUTH) return;
if (pwLen >= PW_LEN) return;
if (nowMs - lastPwCursorBlinkMs < PW_CURSOR_BLINK_MS) return;
lastPwCursorBlinkMs += PW_CURSOR_BLINK_MS;
pwCursorBlinkState = !pwCursorBlinkState;
byte col = pwLen;
if (col > 15) col = 15;
lcd.setCursor(col, 1);
lcd.print(pwCursorBlinkState ? "_" : " ");
}
void eraseAuthBackspace() {
if (pwLen == 0) return;
pwLen--;
pwBuf[pwLen] = '\0';
regenPwMaskAll();
showEraseMaskedQuiet();
}
void eraseAuthFailStart() { startWrongCodeAlert(1); }
void eraseAuthSuccess() {
startSuccessTone();
favClearAll();
startAlertScreen("THE FAVORITES:", "CLEARED & RESET",
ALERT_SHOW_MS_DEFAULT, ALERT_BLINK_MS_DEFAULT);
mode = MODE_NORMAL;
}
// -------------------- C edit list auth mode --------------------
void showFavListEditMaskedQuiet();
void enterFavListEditAuthMode() {
mode = MODE_FAV_LIST_EDIT_AUTH;
pwLen = 0;
pwBuf[0] = '\0';
pwMask[0] = '\0';
pwWrongFlag = 0;
pwEditCursorBlinkState = true;
lastPwEditCursorBlinkMs = nowMs;
pwLastInputMs = nowMs;
showFavListEditMaskedQuiet();
}
void showFavListEditMaskedQuiet() {
if (!lcdCanWrite()) return;
lcd.setCursor(0, 0); lcd.print(F("EDIT FAVORITES: "));
lcd.setCursor(0, 1); lcd.print(F("ENTER PW: "));
lcd.setCursor(9, 1);
for (byte i = 0; i < pwLen && i < PW_LEN; i++) lcd.print(pwMask[i]);
for (byte i = pwLen; i < PW_LEN; i++) lcd.print(' ');
byte col = 9 + pwLen;
if (col > 15) col = 15;
lcd.setCursor(col, 1);
lcd.print(F("_"));
pwEditCursorBlinkState = true;
lastPwEditCursorBlinkMs = nowMs;
}
void favListPwCursorUpdate() {
if (!lcdCanWrite()) return;
if (mode != MODE_FAV_LIST_EDIT_AUTH) return;
if (pwLen >= PW_LEN) return;
if (nowMs - lastPwEditCursorBlinkMs < PW_EDIT_CURSOR_BLINK_MS) return;
lastPwEditCursorBlinkMs += PW_EDIT_CURSOR_BLINK_MS;
pwEditCursorBlinkState = !pwEditCursorBlinkState;
byte col = 9 + pwLen;
if (col > 15) col = 15;
lcd.setCursor(col, 1);
lcd.print(pwEditCursorBlinkState ? "_" : " ");
}
void favListAuthBackspace() {
if (pwLen == 0) return;
pwLen--;
pwBuf[pwLen] = '\0';
regenPwMaskAll();
showFavListEditMaskedQuiet();
}
void favListAuthFailStart() { startWrongCodeAlert(2); }
// -------------------- list edit mode --------------------
void enterFavListEditMode() {
mode = MODE_FAV_LIST_EDIT;
if (editSelIndex >= favCount) editSelIndex = 0;
editScrollPos = 0;
lastEditScrollMs = nowMs;
if (!lcdCanWrite()) return;
lcd.setCursor(0, 0);
if (favCount == 0) {
lcd.print(F("EDIT FAVS EMPTY "));
} else {
char l1[17];
snprintf(l1, sizeof(l1), "EDIT FAV (%u)/%u", (unsigned)(editSelIndex + 1), (unsigned)favCount);
printPad16(l1);
}
int len = favVirtualLen();
if (len <= 0) len = 1;
for (byte i = 0; i < 16; i++) editWin[i] = favCharAt((editScrollPos + i) % len);
editWin[16] = '\0';
lcd.setCursor(0, 1);
lcd.print(editWin);
}
void exitFavListEditToFavMode() { favEnterEditMode(); }
void favListSelectPrev() {
if (favCount == 0) return;
if (editSelIndex == 0) editSelIndex = favCount - 1;
else editSelIndex--;
if (!lcdCanWrite()) return;
lcd.setCursor(0, 0);
char l1[17];
snprintf(l1, sizeof(l1), "EDIT FAV (%u)/%u", (unsigned)(editSelIndex + 1), (unsigned)favCount);
printPad16(l1);
}
void favListSelectNext() {
if (favCount == 0) return;
editSelIndex++;
if (editSelIndex >= favCount) editSelIndex = 0;
if (!lcdCanWrite()) return;
lcd.setCursor(0, 0);
char l1[17];
snprintf(l1, sizeof(l1), "EDIT FAV (%u)/%u", (unsigned)(editSelIndex + 1), (unsigned)favCount);
printPad16(l1);
}
void enterFavItemEditMode() {
if (favCount == 0) return;
mode = MODE_FAV_ITEM_EDIT;
clearBuffer();
uint16_t v = favList[editSelIndex];
char s[4];
snprintf(s, sizeof(s), "%03u", (unsigned)v);
buf[0] = s[0]; buf[1] = s[1]; buf[2] = s[2]; buf[3] = '\0';
bufLen = 3;
entryDirty = true;
if (!lcdCanWrite()) return;
lcd.setCursor(0, 0);
char l1[17];
snprintf(l1, sizeof(l1), "EDIT ITEM (%u)/%u", (unsigned)(editSelIndex + 1), (unsigned)favCount);
printPad16(l1);
lcd.setCursor(0, 1);
lcd.print(F("VAL:___ OK:C "));
lcd.setCursor(4, 1);
lcd.print(buf[0]); lcd.print(buf[1]); lcd.print(buf[2]);
favCursorBlinkState = true;
lastFavCursorBlinkMs = nowMs;
}
void favItemEditCursorBlink() {
if (!lcdCanWrite()) return;
if (bufLen >= 3) return;
if (nowMs - lastFavCursorBlinkMs >= CURSOR_BLINK_MS) {
lastFavCursorBlinkMs = nowMs;
favCursorBlinkState = !favCursorBlinkState;
byte col;
if (bufLen == 0) col = 6;
else if (bufLen == 1) col = 5;
else col = 4;
lcd.setCursor(col, 1);
lcd.print(favCursorBlinkState ? "_" : " ");
}
}
void favItemEditRedrawDigits() {
if (!lcdCanWrite()) return;
lcd.setCursor(0, 1);
lcd.print(F("VAL:___ OK:C "));
char d0 = '_', d1 = '_', d2 = '_';
if (bufLen == 1) d2 = buf[0];
else if (bufLen == 2) { d1 = buf[0]; d2 = buf[1]; }
else if (bufLen >= 3) { d0 = buf[bufLen - 3]; d1 = buf[bufLen - 2]; d2 = buf[bufLen - 1]; }
lcd.setCursor(4, 1);
lcd.print(d0); lcd.print(d1); lcd.print(d2);
}
void saveFavItemEdit() {
if (favCount == 0) { enterFavListEditMode(); return; }
uint16_t v = bufferToNumber();
if (v > 255) {
startAlertScreen("ERR: 0-255 ONLY", "NOT SAVED", 1200, 250);
mode = MODE_NORMAL;
return;
}
favList[editSelIndex] = v;
saveFavoritesToEEPROM();
enterFavListEditMode();
}
// =========================================================
// SLEEP / DIM CONTROL
// =========================================================
void wakeDisplayFromKeypress() {
if (displaySleeping) {
lcd.display();
lcd.backlight();
displaySleeping = false;
}
if (dimmingActive) {
dimmingActive = false;
}
lcdWritesFrozen = false;
lcd.backlight();
}
void noteKeypadTouch() {
lastKeyTouchMs = nowMs;
wakeDisplayFromKeypress();
}
void startDimmingNow() {
dimmingActive = true;
dimStartMs = nowMs;
lastDimPwmMs = 0;
dimPwmPhase = 0;
lcdWritesFrozen = true;
}
void goToSleepNow() {
lcd.noBacklight();
lcd.noDisplay();
displaySleeping = true;
dimmingActive = false;
lcdWritesFrozen = true;
}
void updateBacklightSleep() {
if (displaySleeping) return;
if (!dimmingActive) {
if (lastKeyTouchMs == 0) lastKeyTouchMs = nowMs;
if ((unsigned long)(nowMs - lastKeyTouchMs) >= LCD_IDLE_BEFORE_DIM_MS) {
startDimmingNow();
}
return;
}
unsigned long elapsed = nowMs - dimStartMs;
if (elapsed >= LCD_DIM_DURATION_MS) {
goToSleepNow();
return;
}
uint32_t b = 255UL - (255UL * (uint32_t)elapsed) / (uint32_t)LCD_DIM_DURATION_MS;
byte brightness = (byte)b;
if (lastDimPwmMs == 0) lastDimPwmMs = nowMs;
if ((unsigned long)(nowMs - lastDimPwmMs) < DIM_PWM_PERIOD_MS) return;
lastDimPwmMs += DIM_PWM_PERIOD_MS;
dimPwmPhase++;
if (dimPwmPhase < brightness) lcd.backlight();
else lcd.noBacklight();
}
// =========================================================
// SHARED "A" INPUT HANDLERS
// =========================================================
void handleACommonPress() {
setKeyPulse(true);
if (displaySleeping || dimmingActive || lcdWritesFrozen) {
lastKeyTouchMs = nowMs;
wakeDisplayFromKeypress();
return;
}
noteKeypadTouch();
if (alertActive) return;
appliedUntilMs = 0;
errorUntilMs = 0;
if (mode == MODE_FAV_ERASE_AUTH || mode == MODE_FAV_LIST_EDIT_AUTH) return;
aDown = true;
aDownStartMs = nowMs;
aLongFired = false;
}
void handleACommonRelease() {
setKeyPulse(false);
if (alertActive) return;
aDown = false;
if (!aLongFired && mode == MODE_STEP) {
stepAdvanceOnTrig();
}
}
void handleVirtualAPress() { handleACommonPress(); }
void handleVirtualARelease(){ handleACommonRelease(); }
void updateVirtualAInput() {
int a6 = analogRead(A6_VIRTUAL_A_PIN);
bool newRawPressed = a6RawPressed;
if (!a6RawPressed) {
if (a6 <= A6_PRESS_THRESHOLD) newRawPressed = true;
} else {
if (a6 >= A6_RELEASE_THRESHOLD) newRawPressed = false;
}
if (newRawPressed != a6RawPrev) {
a6RawPrev = newRawPressed;
a6LastEdgeMs = nowMs;
}
if ((unsigned long)(nowMs - a6LastEdgeMs) >= A6_DEBOUNCE_MS) {
if (a6StablePressed != newRawPressed) {
a6StablePressed = newRawPressed;
a6RawPressed = newRawPressed;
if (a6StablePressed) handleVirtualAPress();
else handleVirtualARelease();
}
}
a6RawPressed = newRawPressed;
}
// -------------------- Keypad event handler --------------------
void keypadEvent(KeypadEvent k) {
KeyState st = keypad.getState();
if (st == PRESSED) {
setKeyPulse(true);
if (displaySleeping || dimmingActive || lcdWritesFrozen) {
lastKeyTouchMs = nowMs;
wakeDisplayFromKeypress();
return;
}
noteKeypadTouch();
if (alertActive) return;
appliedUntilMs = 0;
errorUntilMs = 0;
if (mode == MODE_FAV_ERASE_AUTH) {
if (k >= '0' && k <= '9') {
pwLastInputMs = nowMs;
if (pwLen < PW_LEN) {
pwBuf[pwLen++] = k;
pwBuf[pwLen] = '\0';
regenPwMaskAll();
showEraseMaskedQuiet();
if (pwLen >= PW_LEN) {
if (strncmp(pwBuf, PW_CODE, PW_LEN) == 0) eraseAuthSuccess();
else eraseAuthFailStart();
}
}
return;
}
if (k == '#') {
pwLastInputMs = nowMs;
eraseAuthBackspace();
return;
}
return;
}
if (mode == MODE_FAV_LIST_EDIT_AUTH) {
if (k >= '0' && k <= '9') {
pwLastInputMs = nowMs;
if (pwLen < PW_LEN) {
pwBuf[pwLen++] = k;
pwBuf[pwLen] = '\0';
regenPwMaskAll();
showFavListEditMaskedQuiet();
if (pwLen >= PW_LEN) {
if (strncmp(pwBuf, PW_CODE, PW_LEN) == 0) {
startSuccessTone();
enterFavListEditMode();
} else {
favListAuthFailStart();
}
}
}
return;
}
if (k == '#') {
pwLastInputMs = nowMs;
favListAuthBackspace();
return;
}
return;
}
if (k == '*') { starDown = true; starDownStartMs = nowMs; starLongFired = false; return; }
if (k == '#') { if (mode != MODE_STEP) { hashDown = true; hashDownStartMs = nowMs; hashLongFired = false; } return; }
if (k == 'A') { handleACommonPress(); return; }
if (k == 'B') { bDown = true; bDownStartMs = nowMs; bLongFired = false; return; }
if (k == 'C') { cDown = true; cDownStartMs = nowMs; cLongFired = false; return; }
if (k == 'D') { dDown = true; dDownStartMs = nowMs; dLongFired = false; return; }
if (mode == MODE_FAV_LIST_EDIT) {
if (k == '2' || k == '4') { favListSelectPrev(); return; }
if (k == '6' || k == '8') { favListSelectNext(); return; }
return;
}
if (mode == MODE_FAV_ITEM_EDIT) {
if (k >= '0' && k <= '9') {
if (bufLen >= 3) { clearBuffer(); }
appendDigit(k);
favItemEditRedrawDigits();
return;
}
if (k == '#') { backspaceDigit(); favItemEditRedrawDigits(); return; }
return;
}
if (mode == MODE_FAV_EDIT) {
if (k >= '0' && k <= '9') { appendDigit(k); favDrawTypingLine2Once(); return; }
if (k == '#') {
backspaceDigit();
if (bufLen) favDrawTypingLine2Once();
else favDrawPromptOnce();
return;
}
return;
}
if (mode == MODE_FAV_PLAY) return;
if (mode == MODE_REPEAT && (k >= '0' && k <= '9')) {
mode = MODE_NORMAL;
forceOutputsOff();
pulseUntilMs = 0;
playingValue = 0;
clearLine2();
scrollPos = 0;
lastScrollMs = nowMs;
}
if (k >= '0' && k <= '9') {
if (mode == MODE_STEP) {
mode = MODE_NORMAL;
initLine1();
forceOutputsOff();
pulseUntilMs = 0;
playingValue = 0;
}
appendDigit(k);
typingUntilMs = nowMs + TYPING_HOLD_MS;
showTypedDigitsLine2();
scrollPos = 0;
lastScrollMs = nowMs;
return;
}
return;
}
if (st == RELEASED) {
setKeyPulse(false);
if (alertActive) return;
if (k == '*') {
starDown = false;
if (starLongFired) return;
if (mode == MODE_FAV_ERASE_AUTH || mode == MODE_FAV_LIST_EDIT_AUTH) {
favEnterEditMode();
return;
}
if (mode == MODE_FAV_PLAY || mode == MODE_FAV_EDIT ||
mode == MODE_FAV_LIST_EDIT || mode == MODE_FAV_ITEM_EDIT) {
if (mode == MODE_FAV_LIST_EDIT || mode == MODE_FAV_ITEM_EDIT) exitFavListEditToFavMode();
else fullResetToNormal();
return;
}
fullResetToNormal();
return;
}
if (k == '#') {
hashDown = false;
if (hashLongFired) return;
if (mode == MODE_STEP) return;
if (mode == MODE_FAV_EDIT || mode == MODE_FAV_PLAY || mode == MODE_FAV_ERASE_AUTH ||
mode == MODE_FAV_LIST_EDIT_AUTH || mode == MODE_FAV_LIST_EDIT || mode == MODE_FAV_ITEM_EDIT) return;
if (entryDirty) {
uint16_t v = bufferToNumber();
clearBuffer();
typingUntilMs = 0;
if (v <= 255) startPulsePlay(v);
else {
lastErrorValue = v;
errorUntilMs = nowMs + ERROR_SHOW_MS;
errorScrollPos = 0;
clearLine2();
lastScrollMs = nowMs;
}
return;
}
if (mode == MODE_NORMAL && playingValue == 0) {
if (forgetPrev) fullResetToNormal();
else startPulsePlay(lastPlayedValue);
}
return;
}
if (k == 'A') {
handleACommonRelease();
return;
}
if (k == 'B') {
bDown = false;
if (bLongFired) return;
if (mode == MODE_FAV_PLAY) { favAdvance(); return; }
if (mode == MODE_FAV_EDIT) {
if (!entryDirty || bufLen == 0) {
favDrawPromptOnce();
if (lcdCanWrite()) {
lcd.setCursor(0, 1);
lcd.print(F("TYPE 0-255 "));
}
favMsgUntilMs = nowMs + 900;
return;
}
uint16_t v = bufferToNumber();
clearBuffer();
if (v <= 255) {
if (favAdd(v)) {
favDrawLine1Once();
if (lcdCanWrite()) { lcd.setCursor(0, 1); lcd.print(F("SAVED ")); }
favMsgUntilMs = nowMs + 700;
} else {
favDrawLine1Once();
if (lcdCanWrite()) { lcd.setCursor(0, 1); lcd.print(F("FAV FULL (MAX10)")); }
favMsgUntilMs = nowMs + 1200;
}
} else {
favDrawLine1Once();
if (lcdCanWrite()) { lcd.setCursor(0, 1); lcd.print(F("ERR: 0-255 ONLY ")); }
favMsgUntilMs = nowMs + 1200;
}
return;
}
return;
}
if (k == 'C') {
cDown = false;
if (cLongFired) return;
if (mode == MODE_FAV_LIST_EDIT) { enterFavItemEditMode(); return; }
if (mode == MODE_FAV_ITEM_EDIT) { saveFavItemEdit(); return; }
return;
}
if (k == 'D') {
dDown = false;
if (!dLongFired) {
if (mode == MODE_FAV_EDIT) favShowCountAndLast();
}
return;
}
}
}
// -------------------- Setup --------------------
void setup() {
// Set safe initial states before making outputs active
pinMode(KEYPULSE_PIN, OUTPUT);
digitalWrite(KEYPULSE_PIN, LOW);
keyPulseState = false;
for (byte i = 0; i < 8; i++) {
if (outPins[i] != KEYPULSE_PIN) {
pinMode(outPins[i], OUTPUT);
}
}
pinMode(TRIG_PIN, INPUT);
pinMode(A6_VIRTUAL_A_PIN, INPUT);
forceOutputsOff();
lcd.init();
lcd.backlight();
lcd.display();
randomSeed(analogRead(A7) ^ micros());
loadFavoritesFromEEPROM();
initLine1();
clearLine2();
keypad.addEventListener(keypadEvent);
keypad.setHoldTime(200);
lastScrollMs = millis();
initTriggerFilter();
pwBuf[0] = '\0';
pwMask[0] = '\0';
pwLen = 0;
lastKeyTouchMs = millis();
a6LastEdgeMs = millis();
startupMs = millis();
startupPulseStartMs = 0;
startupReady = false;
startupPulseDone = false;
}
// -------------------- Loop --------------------
void loop() {
nowMs = millis();
if (!startupReady) {
setKeyPulse(false);
// short boot settle time first
if ((unsigned long)(nowMs - startupMs) < STARTUP_LOCKOUT_MS) {
forceOutputsOff();
keypad.getKey();
updateBacklightSleep();
return;
}
// startup MP3 pulse: D1 LOW for 1 second, then reset
if (!startupPulseDone) {
if (startupPulseStartMs == 0) {
startupPulseStartMs = nowMs;
setOutputsFromValue(STARTUP_MP3_VALUE); // D1 LOW only
}
if ((unsigned long)(nowMs - startupPulseStartMs) < STARTUP_MP3_PULSE_MS) {
keypad.getKey();
updateBacklightSleep();
return;
}
forceOutputsOff();
startupPulseDone = true;
startupReady = true;
fullResetToNormal();
}
}
keypad.getKey();
updateVirtualAInput();
updateBacklightSleep();
updateAlarmTone();
updateSuccessTone();
if (alertActive) {
updateAlertScreen();
if (!alertActive) {
stopAlarmTone();
initLine1();
clearLine2();
resetVisualTimers();
if (pwWrongFlag == 2) { pwWrongFlag = 0; enterFavListEditAuthMode(); }
else if (pwWrongFlag == 1) { pwWrongFlag = 0; enterEraseAuthMode(); }
}
return;
}
if (mode == MODE_FAV_ERASE_AUTH || mode == MODE_FAV_LIST_EDIT_AUTH) {
if (pwLastInputMs && (unsigned long)(nowMs - pwLastInputMs) >= PW_IDLE_TIMEOUT_MS) {
favEnterEditMode();
}
}
if (starDown && !starLongFired && (nowMs - starDownStartMs >= STAR_HOLD_MS)) {
starLongFired = true;
longStarForgetReset();
hashDown = false; hashLongFired = true;
aDown = false; aLongFired = true;
bDown = false; bLongFired = true;
cDown = false; cLongFired = true;
dDown = false; dLongFired = true;
}
if (aDown && !aLongFired && (nowMs - aDownStartMs >= A_HOLD_MS)) {
aLongFired = true;
uint16_t startVal;
if (entryDirty && bufLen > 0) {
uint16_t typedVal = bufferToNumber();
startVal = (typedVal > 0) ? typedVal : 1;
} else {
startVal = (forgetPrev) ? 1 : ((lastPlayedValue > 0) ? lastPlayedValue : 1);
}
enterStepMode(startVal);
hashDown = false;
hashLongFired = true;
}
if (bDown && !bLongFired && (nowMs - bDownStartMs >= B_HOLD_MS)) {
bLongFired = true;
if (mode == MODE_FAV_PLAY) {
fullResetToNormal();
} else if (mode == MODE_FAV_EDIT) {
favEnterPlayMode();
} else if (mode == MODE_FAV_ERASE_AUTH) {
fullResetToNormal();
} else if (mode == MODE_FAV_LIST_EDIT_AUTH || mode == MODE_FAV_LIST_EDIT || mode == MODE_FAV_ITEM_EDIT) {
exitFavListEditToFavMode();
} else {
forceOutputsOff();
pulseUntilMs = 0;
favEnterEditMode();
}
hashDown = false; hashLongFired = true;
dDown = false; dLongFired = true;
}
if (cDown && !cLongFired && (nowMs - cDownStartMs >= C_HOLD_MS)) {
cLongFired = true;
if (mode == MODE_FAV_LIST_EDIT || mode == MODE_FAV_ITEM_EDIT || mode == MODE_FAV_LIST_EDIT_AUTH) {
exitFavListEditToFavMode();
} else if (mode == MODE_FAV_EDIT) {
enterFavListEditAuthMode();
}
}
if (mode != MODE_FAV_EDIT && mode != MODE_FAV_PLAY && mode != MODE_FAV_ERASE_AUTH &&
mode != MODE_FAV_LIST_EDIT_AUTH && mode != MODE_FAV_LIST_EDIT && mode != MODE_FAV_ITEM_EDIT) {
if (hashDown && !hashLongFired && (nowMs - hashDownStartMs >= HASH_HOLD_MS)) {
hashLongFired = true;
if (mode != MODE_STEP) {
uint16_t v = entryDirty ? bufferToNumber() : (forgetPrev ? 0 : lastPlayedValue);
if (v <= 255) enterRepeatMode(v);
else {
lastErrorValue = v;
errorUntilMs = nowMs + ERROR_SHOW_MS;
errorScrollPos = 0;
clearLine2();
lastScrollMs = nowMs;
}
}
}
}
if (dDown && !dLongFired && (nowMs - dDownStartMs >= D_HOLD_MS)) {
dLongFired = true;
if (mode == MODE_FAV_EDIT) {
enterEraseAuthMode();
}
}
if (pulseUntilMs && nowMs >= pulseUntilMs) {
pulseUntilMs = 0;
if (mode != MODE_REPEAT) forceOutputsOff();
}
// ---------------- D0: STRICT LOW->HIGH ONLY ----------------
if (updateTriggerRisingEdge()) {
if (mode == MODE_STEP) stepAdvanceOnTrig();
else if (mode == MODE_FAV_PLAY) favAdvance();
else if (mode == MODE_REPEAT) { /* ignored */ }
else fullResetToNormal();
}
if (lcdWritesFrozen) return;
if (mode == MODE_FAV_ERASE_AUTH) { erasePwCursorUpdate(); return; }
if (mode == MODE_FAV_LIST_EDIT_AUTH) { favListPwCursorUpdate(); return; }
if (mode == MODE_FAV_LIST_EDIT) {
if (nowMs - lastEditScrollMs >= 260) {
lastEditScrollMs = nowMs;
int len = favVirtualLen();
if (len <= 0) len = 1;
if (editScrollPos >= len) editScrollPos = 0;
for (byte i = 0; i < 16; i++) editWin[i] = favCharAt((editScrollPos + i) % len);
editWin[16] = '\0';
lcd.setCursor(0, 1);
lcd.print(editWin);
editScrollPos++;
}
return;
}
if (mode == MODE_FAV_ITEM_EDIT) {
favItemEditCursorBlink();
return;
}
if (mode == MODE_FAV_EDIT) {
if (favMsgUntilMs && nowMs >= favMsgUntilMs) {
favMsgUntilMs = 0;
favBlinkState = true;
lastFavBlinkMs = nowMs;
favCursorBlinkState = true;
lastFavCursorBlinkMs = nowMs;
favBBlinkState = true;
lastFavBBlinkMs = nowMs;
favDrawPromptOnce();
}
if (favMsgUntilMs) return;
if (bufLen > 0) {
favDrawTypingLine2Once();
favBlinkTypingCursorUpdate();
favBlinkTypingSaveUpdate();
} else {
if (favTypingView) {
favBlinkState = true;
lastFavBlinkMs = nowMs;
favDrawPromptOnce();
}
favBlinkPromptUpdate();
}
return;
}
if (mode == MODE_FAV_PLAY) {
if (favCount == 0) { fullResetToNormal(); return; }
if (nowMs - lastScrollMs >= SCROLL_STEP_MS) {
lastScrollMs = nowMs;
buildFavNowText(scrollBuf, sizeof(scrollBuf), favIndex, favCount, playingValue);
showScrollWindow(scrollBuf, scrollPos);
scrollPos++;
}
return;
}
if (mode != MODE_STEP) updateLine1Blink();
if (errorUntilMs) {
if (nowMs < errorUntilMs) {
if (nowMs - lastScrollMs >= ERROR_SCROLL_STEP_MS) {
lastScrollMs = nowMs;
buildErrorScrollText(scrollBuf, sizeof(scrollBuf), lastErrorValue);
showScrollWindow(scrollBuf, errorScrollPos);
errorScrollPos++;
}
return;
} else {
errorUntilMs = 0;
clearLine2();
scrollPos = 0;
lastScrollMs = nowMs;
}
}
if (appliedUntilMs) {
if (nowMs < appliedUntilMs) return;
appliedUntilMs = 0;
clearLine2();
scrollPos = 0;
lastScrollMs = nowMs;
}
if (nowMs < typingUntilMs) {
showTypedDigitsLine2();
return;
}
if (nowMs - lastScrollMs >= SCROLL_STEP_MS) {
lastScrollMs = nowMs;
if (mode == MODE_STEP) buildStepScrollText(scrollBuf, sizeof(scrollBuf), stepCurrent);
else if (mode == MODE_REPEAT) buildRepeatScrollText(scrollBuf, sizeof(scrollBuf), playingValue);
else buildNormalScrollText(scrollBuf, sizeof(scrollBuf), playingValue);
showScrollWindow(scrollBuf, scrollPos);
scrollPos++;
}
}