#include <EEPROM.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Encoder.h>
#include <RCSwitch.h>
// --- EEPROM Configuration ---
#define EEPROM_MAGIC_NUMBER 0xFD
#define EEPROM_APP_VERSION 1
#define EEPROM_MAGIC_ADDR 0
#define EEPROM_VERSION_ADDR 1
#define EEPROM_CUES_START_ADDR 2
// --- LCD Configuration ---
#define LCD_COLS 20
#define LCD_ROWS 4
LiquidCrystal_I2C lcd(0x27, LCD_COLS, LCD_ROWS); // Primary GUI
LiquidCrystal_I2C cueLcd(0x26, LCD_COLS, LCD_ROWS); // Status Grid Monitor
// --- Encoder Configuration ---
#define ENCODER_CLK_PIN 2
#define ENCODER_DT_PIN 3
#define ENCODER_SW_PIN 4
Encoder rotaryEncoder(ENCODER_CLK_PIN, ENCODER_DT_PIN);
long encoderPos = 0;
long oldEncoderPos = -999;
bool encoderButtonPressed = false;
bool lastButtonState = HIGH;
unsigned long lastEncoderButtonProcessTime = 0;
#define ENCODER_DEBOUNCE_DELAY 50
// --- 433Mhz Wireless Configuration ---
#define TX_PIN 10
RCSwitch mySwitch = RCSwitch();
unsigned long bilusocnCodes[12] = {
5592513, 5592514, 5592515, 5592516,
5592517, 5592518, 5592519, 5592520,
5592521, 5592522, 5592523, 5592524
};
// --- Key Switch & Master Relay Pins ---
#define SAFE_MODE_PIN 5
#define MANUAL_ARMED_PIN 6
#define SHOW_ARMED_PIN 7
#define KEY_SWITCH_ACTIVE LOW
#define MASTER_POWER_RELAY_PIN 46
#define MASTER_ON LOW
#define MASTER_OFF HIGH
#define NUM_RELAYS 12
const int manualFireButtonPins[NUM_RELAYS] = {34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45};
unsigned long lastManualFirePressTime[NUM_RELAYS];
#define MANUAL_FIRE_DEBOUNCE_DELAY 50
#define GLOBAL_FIRE_LOCKOUT 1000
#define RELAY_PULSE_DURATION 2000
unsigned long relayOffTime[NUM_RELAYS];
unsigned long lastGlobalFireTime = 0;
unsigned long firingMessageClearTime = 0;
enum SystemMode {
MODE_STARTUP, MODE_SAFE, MODE_PROGRAM_CUES_INTERACTIVE,
MODE_MANUAL_ARMED, MODE_SHOW_ARMED, MODE_SHOW_CONFIRM_START,
MODE_SHOW_COUNTDOWN, MODE_SHOW_RUNNING, MODE_SHOW_COMPLETE
};
SystemMode currentMode = MODE_STARTUP;
unsigned long cueFireTimes[NUM_RELAYS];
bool cueHasFired[NUM_RELAYS];
int sortedIndices[NUM_RELAYS];
#define PROG_CUES_PER_PAGE (LCD_ROWS - 1)
int prog_cursorCueIndex = 0;
int prog_editingCueIndex = -1;
unsigned long prog_editBufferTime = 0;
int prog_displayPageOffset = 0;
int confirmStartSelection = 0;
unsigned long showStartTime, countdownStartTime;
unsigned long currentShowTimeSec = 0;
int activeCuesInShowCount = 0, firedCuesCount = 0;
bool showStartedWithNoCues = false;
unsigned long nextAllowedFireTimeSec = 0;
void updateDisplay();
void sortCuesChronologically() {
for (int i = 0; i < NUM_RELAYS; i++) sortedIndices[i] = i;
for (int i = 0; i < NUM_RELAYS - 1; i++) {
for (int j = 0; j < NUM_RELAYS - i - 1; j++) {
unsigned long t1 = cueFireTimes[sortedIndices[j]];
unsigned long t2 = cueFireTimes[sortedIndices[j+1]];
unsigned long v1 = (t1 == 0) ? 999999 : t1;
unsigned long v2 = (t2 == 0) ? 999999 : t2;
if (v1 > v2) {
int temp = sortedIndices[j];
sortedIndices[j] = sortedIndices[j+1];
sortedIndices[j+1] = temp;
}
}
}
}
void drawSingleCueStatus(int idx, char symbol) {
int row = idx % 4;
int colStart = (idx < 4) ? 0 : (idx < 8 ? 7 : 14);
int markerOffset = (idx + 1 > 9) ? 4 : 3;
cueLcd.setCursor(colStart + markerOffset, row);
cueLcd.print(symbol);
}
void refreshCueMonitor() {
bool active = (currentMode == MODE_SHOW_RUNNING || currentMode == MODE_SHOW_COUNTDOWN || currentMode == MODE_SHOW_COMPLETE || currentMode == MODE_MANUAL_ARMED);
if (!active) {
static bool cueLcdCleared = false;
if (!cueLcdCleared) { cueLcd.clear(); cueLcdCleared = true; }
return;
}
static unsigned long lastRefresh = 0;
if (millis() - lastRefresh < 400) return;
lastRefresh = millis();
for (int i = 0; i < NUM_RELAYS; i++) {
int row = i % 4;
int col = (i < 4) ? 0 : (i < 8 ? 7 : 14);
cueLcd.setCursor(col, row);
char status = (currentMode == MODE_MANUAL_ARMED) ? ((relayOffTime[i] > millis()) ? 'X' : ' ') : (cueHasFired[i] ? 'X' : ' ');
cueLcd.print("C"); cueLcd.print(i + 1); cueLcd.print("-"); cueLcd.print(status);
if (i < 8) {
cueLcd.setCursor(col + (i + 1 > 9 ? 6 : 5), row);
cueLcd.print("|");
}
}
}
void saveShowToEEPROM() {
EEPROM.update(EEPROM_MAGIC_ADDR, EEPROM_MAGIC_NUMBER);
EEPROM.update(EEPROM_VERSION_ADDR, EEPROM_APP_VERSION);
for (int i = 0; i < NUM_RELAYS; i++) {
EEPROM.put(EEPROM_CUES_START_ADDR + (i * sizeof(unsigned long)), cueFireTimes[i]);
}
}
void loadShowFromEEPROM() {
byte magic = EEPROM.read(EEPROM_MAGIC_ADDR);
byte version = EEPROM.read(EEPROM_VERSION_ADDR);
if (magic == EEPROM_MAGIC_NUMBER && version == EEPROM_APP_VERSION) {
for (int i = 0; i < NUM_RELAYS; i++) {
EEPROM.get(EEPROM_CUES_START_ADDR + (i * sizeof(unsigned long)), cueFireTimes[i]);
}
} else {
for (int i = 0; i < NUM_RELAYS; i++) cueFireTimes[i] = 0;
saveShowToEEPROM();
}
}
void updateMasterRelayState() {
if (currentMode == MODE_MANUAL_ARMED || currentMode == MODE_SHOW_RUNNING) {
if (digitalRead(MASTER_POWER_RELAY_PIN) != MASTER_ON) Serial.println("Master Power Relay: ON");
digitalWrite(MASTER_POWER_RELAY_PIN, MASTER_ON);
} else {
if (digitalRead(MASTER_POWER_RELAY_PIN) != MASTER_OFF) Serial.println("Master Power Relay: OFF");
digitalWrite(MASTER_POWER_RELAY_PIN, MASTER_OFF);
}
}
void startRelayPulse(int relayIndex) {
if (relayIndex >= 0 && relayIndex < NUM_RELAYS) {
// --- Added Debug Logic for RF Codes ---
Serial.print(F("RF SEND -> Cue: "));
Serial.print(relayIndex + 1);
Serial.print(F(" | Wireless Code: "));
Serial.println(bilusocnCodes[relayIndex]);
// --------------------------------------
lastGlobalFireTime = millis();
firingMessageClearTime = millis() + RELAY_PULSE_DURATION;
lcd.setCursor(0, 2);
lcd.print("Firing: CUE "); lcd.print(relayIndex + 1); lcd.print(" ");
drawSingleCueStatus(relayIndex, 'X');
mySwitch.send(bilusocnCodes[relayIndex], 24);
digitalWrite(TX_PIN, LOW);
if (currentMode == MODE_MANUAL_ARMED) {
cueHasFired[relayIndex] = false;
relayOffTime[relayIndex] = millis() + 500;
} else {
cueHasFired[relayIndex] = true;
relayOffTime[relayIndex] = millis() + RELAY_PULSE_DURATION;
}
}
}
void handleStateTransitions(bool trigger) {
if (!trigger) return;
switch (currentMode) {
case MODE_SAFE:
currentMode = MODE_PROGRAM_CUES_INTERACTIVE;
prog_cursorCueIndex = 0; prog_editingCueIndex = -1;
rotaryEncoder.write(0); updateDisplay();
break;
case MODE_PROGRAM_CUES_INTERACTIVE:
if (prog_editingCueIndex != -1) {
cueFireTimes[prog_editingCueIndex] = prog_editBufferTime;
saveShowToEEPROM(); prog_editingCueIndex = -1;
rotaryEncoder.write(-prog_cursorCueIndex * 4L); encoderPos = prog_cursorCueIndex;
} else {
prog_editingCueIndex = prog_cursorCueIndex;
prog_editBufferTime = cueFireTimes[prog_editingCueIndex];
rotaryEncoder.write(-prog_editBufferTime * 4L); encoderPos = prog_editBufferTime;
}
updateDisplay();
break;
case MODE_SHOW_ARMED:
currentMode = MODE_SHOW_CONFIRM_START;
confirmStartSelection = 0; rotaryEncoder.write(0); encoderPos = 0; updateDisplay();
break;
case MODE_SHOW_CONFIRM_START:
if (confirmStartSelection == 0) {
activeCuesInShowCount = 0;
for (int i = 0; i < NUM_RELAYS; i++) if (cueFireTimes[i] > 0) activeCuesInShowCount++;
if (activeCuesInShowCount > 0) {
showStartedWithNoCues = false;
sortCuesChronologically(); currentMode = MODE_SHOW_COUNTDOWN; countdownStartTime = millis();
} else {
showStartedWithNoCues = true;
currentMode = MODE_SHOW_COMPLETE;
updateDisplay();
}
} else { currentMode = MODE_SHOW_ARMED; updateDisplay(); }
break;
case MODE_SHOW_COMPLETE:
showStartedWithNoCues = false;
currentMode = (digitalRead(SHOW_ARMED_PIN) == LOW) ? MODE_SHOW_ARMED : MODE_SAFE;
updateDisplay();
break;
}
}
void setup() {
Serial.begin(115200);
mySwitch.enableTransmit(TX_PIN);
mySwitch.setProtocol(1);
mySwitch.setRepeatTransmit(12);
digitalWrite(MASTER_POWER_RELAY_PIN, MASTER_OFF);
pinMode(MASTER_POWER_RELAY_PIN, OUTPUT);
for (int i = 0; i < NUM_RELAYS; i++) pinMode(manualFireButtonPins[i], INPUT_PULLUP);
lcd.init(); lcd.backlight();
cueLcd.init(); cueLcd.backlight();
loadShowFromEEPROM();
pinMode(SAFE_MODE_PIN, INPUT_PULLUP);
pinMode(MANUAL_ARMED_PIN, INPUT_PULLUP);
pinMode(SHOW_ARMED_PIN, INPUT_PULLUP);
pinMode(ENCODER_SW_PIN, INPUT_PULLUP);
updateDisplay();
delay(1000);
while (digitalRead(SAFE_MODE_PIN) != LOW) { delay(100); }
currentMode = MODE_SAFE;
updateDisplay();
}
void loop() {
readEncoderButton();
handleEncoderRotation();
handleKeySwitch();
unsigned long cur = millis();
for (int i = 0; i < NUM_RELAYS; i++) {
if (relayOffTime[i] != 0 && cur >= relayOffTime[i]) {
relayOffTime[i] = 0;
if(currentMode == MODE_MANUAL_ARMED) drawSingleCueStatus(i, ' ');
}
}
refreshCueMonitor();
bool processTrigger = encoderButtonPressed;
if (encoderButtonPressed) encoderButtonPressed = false;
handleStateTransitions(processTrigger);
if (currentMode == MODE_MANUAL_ARMED) handleManualFireButtons();
else if (currentMode == MODE_SHOW_RUNNING) runShowSequence();
else if (currentMode == MODE_SHOW_COUNTDOWN) {
unsigned long elapsed = millis() - countdownStartTime;
if (elapsed >= 5000) {
currentMode = MODE_SHOW_RUNNING;
showStartTime = millis(); currentShowTimeSec = 0; firedCuesCount = 0;
nextAllowedFireTimeSec = 0;
for (int i = 0; i < NUM_RELAYS; i++) cueHasFired[i] = false;
} else {
static int lastCount = -1;
int rem = 5 - (elapsed / 1000);
if (rem != lastCount) {
lcd.clear();
lcd.setCursor(0, 0); lcd.print("--- SHOW STARTING --");
lcd.setCursor(0, 1); lcd.print(" GET READY!");
lcd.setCursor(0, 2); lcd.print(" STARTING IN "); lcd.print(rem); lcd.print("s ");
lcd.setCursor(0, 3); lcd.print("*SAFE KEY TO ABORT* ");
lastCount = rem;
}
}
}
updateMasterRelayState();
}
void readEncoderButton() {
bool currentState = digitalRead(ENCODER_SW_PIN);
if (currentState == LOW && lastButtonState == HIGH) {
if (millis() - lastEncoderButtonProcessTime > ENCODER_DEBOUNCE_DELAY) {
encoderButtonPressed = true; lastEncoderButtonProcessTime = millis();
}
}
lastButtonState = currentState;
}
void handleEncoderRotation() {
long currentLogicalPos = -(rotaryEncoder.read() / 4L);
if (currentLogicalPos != encoderPos) {
encoderPos = currentLogicalPos;
if (currentMode == MODE_PROGRAM_CUES_INTERACTIVE) {
if (prog_editingCueIndex != -1) {
if (encoderPos < 0) { rotaryEncoder.write(0); encoderPos = 0; }
prog_editBufferTime = encoderPos;
} else {
prog_cursorCueIndex = (encoderPos % NUM_RELAYS + NUM_RELAYS) % NUM_RELAYS;
prog_displayPageOffset = prog_cursorCueIndex / PROG_CUES_PER_PAGE;
}
updateDisplay();
} else if (currentMode == MODE_SHOW_CONFIRM_START) {
confirmStartSelection = (encoderPos % 2 + 2) % 2; updateDisplay();
}
}
}
void handleKeySwitch() {
bool safeKey = (digitalRead(SAFE_MODE_PIN) == LOW);
bool manualKey = (digitalRead(MANUAL_ARMED_PIN) == LOW);
bool showKey = (digitalRead(SHOW_ARMED_PIN) == LOW);
if ((currentMode == MODE_SHOW_RUNNING || currentMode == MODE_SHOW_COUNTDOWN) && safeKey) {
currentMode = MODE_SAFE;
lcd.clear(); cueLcd.clear();
updateDisplay();
return;
}
SystemMode desired = currentMode;
if (safeKey) { if (currentMode != MODE_SAFE && currentMode != MODE_PROGRAM_CUES_INTERACTIVE) desired = MODE_SAFE; }
else if (manualKey) { if (currentMode != MODE_MANUAL_ARMED) desired = MODE_MANUAL_ARMED; }
else if (showKey) {
if (currentMode != MODE_SHOW_ARMED && currentMode != MODE_SHOW_CONFIRM_START && currentMode != MODE_SHOW_COMPLETE && currentMode != MODE_SHOW_RUNNING && currentMode != MODE_SHOW_COUNTDOWN)
desired = MODE_SHOW_ARMED;
}
if (desired != currentMode) {
if (desired == MODE_MANUAL_ARMED || desired == MODE_SHOW_ARMED) {
for(int i=0; i<NUM_RELAYS; i++) { cueHasFired[i] = false; relayOffTime[i] = 0; }
cueLcd.clear();
}
if (currentMode == MODE_PROGRAM_CUES_INTERACTIVE) prog_editingCueIndex = -1;
currentMode = desired;
updateDisplay();
}
}
void handleManualFireButtons() {
unsigned long now = millis();
for (int i = 0; i < NUM_RELAYS; i++) {
if (digitalRead(manualFireButtonPins[i]) == LOW && (now - lastManualFirePressTime[i] > 50)) {
if (now - lastGlobalFireTime >= GLOBAL_FIRE_LOCKOUT) {
lastGlobalFireTime = now; lastManualFirePressTime[i] = now;
startRelayPulse(i);
}
}
}
}
void formatTimeToString(char* buffer, unsigned long totalSeconds) {
if (totalSeconds == 0) sprintf(buffer, "OFF");
else if (totalSeconds < 60) sprintf(buffer, "%lus", totalSeconds);
else sprintf(buffer, "%lu:%02lu", totalSeconds / 60, totalSeconds % 60);
}
void drawCueListLine(int lcdLine, int cueIndex, bool hasCursor, bool isBeingEdited, unsigned long editBufferTime) {
char lineBuffer[21]; char timeStr[10];
lcd.setCursor(0, lcdLine);
if (cueIndex >= NUM_RELAYS) { lcd.print(" "); return; }
unsigned long timeVal = isBeingEdited ? editBufferTime : cueFireTimes[cueIndex];
formatTimeToString(timeStr, timeVal);
sprintf(lineBuffer, "%c CUE %02d: %-7s(%c)", (hasCursor ? '>' : ' '), cueIndex + 1, timeStr, (cueFireTimes[cueIndex] > 0 ? 'P' : '-'));
if (isBeingEdited) sprintf(lineBuffer, "> CUE %02d: [%-6s] ", cueIndex + 1, timeStr);
lcd.print(lineBuffer);
}
void updateDisplay() {
char lineBuffer[21];
if (currentMode == MODE_SHOW_RUNNING || currentMode == MODE_SHOW_COUNTDOWN) return;
switch (currentMode) {
case MODE_STARTUP:
lcd.clear(); lcd.setCursor(0, 0); lcd.print("BOOTING SYSTEM...");
lcd.setCursor(0, 2); lcd.print("SET KEY TO SAFE");
lcd.setCursor(0, 3); lcd.print("TO CONTINUE...");
break;
case MODE_SAFE:
lcd.clear(); lcd.setCursor(0, 0); lcd.print("-----SAFE MODE------");
lcd.setCursor(0, 1); lcd.print(" [SYSTEM IS SAFE]");
lcd.setCursor(0, 2); lcd.print(" PRESS ENCODER KNOB");
lcd.setCursor(0, 3); lcd.print(" TO PROGRAM CUES.");
break;
case MODE_PROGRAM_CUES_INTERACTIVE:
lcd.clear(); lcd.setCursor(0, 0); lcd.print("PROGRAM CUES (Pg"); lcd.print((prog_cursorCueIndex / PROG_CUES_PER_PAGE) + 1); lcd.print("/4)");
for (int i = 0; i < PROG_CUES_PER_PAGE; i++) {
int cueIdx = (prog_cursorCueIndex / PROG_CUES_PER_PAGE) * PROG_CUES_PER_PAGE + i;
drawCueListLine(i + 1, cueIdx, cueIdx == prog_cursorCueIndex, cueIdx == prog_editingCueIndex, prog_editBufferTime);
}
break;
case MODE_MANUAL_ARMED:
lcd.clear(); lcd.setCursor(0, 0); lcd.print("-MANUAL FIRE ARMED-");
lcd.setCursor(0, 1); lcd.print("FIRING RELAYS ARE ON");
lcd.setCursor(0, 3); lcd.print("PRESS FIRE BUTTONS");
break;
case MODE_SHOW_ARMED:
lcd.clear(); lcd.setCursor(0, 0); lcd.print("-----SHOW ARMED-----");
lcd.setCursor(0, 2); lcd.print(" PRESS ENCODER KNOB");
lcd.setCursor(0, 3); lcd.print(" TO START SHOW.");
break;
case MODE_SHOW_CONFIRM_START:
lcd.clear(); lcd.setCursor(0, 0); lcd.print("CONFIRM SHOW START?");
lcd.setCursor(0, 1); lcd.print(confirmStartSelection == 0 ? "> YES" : " YES");
lcd.setCursor(0, 2); lcd.print(confirmStartSelection == 1 ? "> NO " : " NO ");
break;
case MODE_SHOW_COMPLETE:
lcd.clear();
if (showStartedWithNoCues) {
lcd.setCursor(0, 0); lcd.print("!NO CUES PROGRAMMED!");
lcd.setCursor(0, 1); lcd.print(" CANNOT START SHOW ");
lcd.setCursor(0, 2); lcd.print(" PRESS ENCODER KNOB ");
lcd.setCursor(0, 3); lcd.print(" TO EXIT ");
} else {
lcd.setCursor(0, 0); lcd.print(" SHOW COMPLETE! ");
lcd.setCursor(0, 1); lcd.print(" ALL CUES FIRED ");
lcd.setCursor(0, 2); lcd.print(" PRESS ENCODER KNOB ");
lcd.setCursor(0, 3); lcd.print(" TO EXIT ");
}
break;
}
}
void runShowSequence() {
if (currentMode != MODE_SHOW_RUNNING) return;
static unsigned long lastInit = 0;
if (showStartTime != lastInit) {
lastInit = showStartTime;
lcd.clear();
lcd.setCursor(0, 0); lcd.print(" SHOW RUNNING! ");
lcd.setCursor(0, 3); lcd.print("*SAFE KEY TO ABORT* ");
}
unsigned long nowSec = (millis() - showStartTime) / 1000;
currentShowTimeSec = nowSec;
int nextCueIdx = -1;
for (int i = 0; i < NUM_RELAYS; i++) {
int idx = sortedIndices[i];
if (!cueHasFired[idx] && cueFireTimes[idx] > 0) { nextCueIdx = idx; break; }
}
lcd.setCursor(0, 2);
if (nextCueIdx != -1) {
long diff = (long)cueFireTimes[nextCueIdx] - (long)currentShowTimeSec;
char countBuf[21];
sprintf(countBuf, "Next: CUE %d in %02lds ", nextCueIdx + 1, (diff < 0 ? 0 : diff));
lcd.print(countBuf);
} else {
lcd.print(" Show Finishing ");
}
if (firingMessageClearTime != 0 && millis() >= firingMessageClearTime) {
lcd.setCursor(0, 1); lcd.print(" ");
firingMessageClearTime = 0;
}
for (int i = 0; i < NUM_RELAYS; i++) {
if (cueFireTimes[i] > 0 && !cueHasFired[i] && currentShowTimeSec >= cueFireTimes[i] && currentShowTimeSec >= nextAllowedFireTimeSec) {
startRelayPulse(i);
firedCuesCount++;
nextAllowedFireTimeSec = currentShowTimeSec + 1;
}
}
if (activeCuesInShowCount > 0 && firedCuesCount >= activeCuesInShowCount) {
currentMode = MODE_SHOW_COMPLETE;
updateDisplay();
}
}MODE SWITCHES (REPLACE WITH MULTI-POSITION SWITCH)
SAFE = PIN 5, MANUAL = PIN 6, SHOW = PIN 7
MANUAL FIRING BUTTONS 1-12 (PINS 33-45)
SAFE
MANUAL
SHOW
MASTER POWER
RELAY - PIN 46
10k Pull-up
(Failsafe)
PIN 2
PIN 3
PIN 4
+5V
GND
GND
+5V
PIN 20
PIN 21
KY-040
20x4 i2c
USE ACTIVE LOW RELAY
433MHz TRANSMITTER
20x4 i2c
+5V WILL RUN THROUGH THE MASTER RELAY (COM, NO) AND
CONNECT TO THE VCC PIN OF THE 433MHZ MODULE. GROUND OF
THE 433MHZ MODULE GOES TO ARDUINO GROUND.
DATA CONNECTS TO PIN 10 SIMULATED BY THE BLUE LED.
220-330 ohm