/**
* SHOWDUINO ESP32-S3 PROFESSIONAL SHOW CONTROLLER v2.7
* Complete system with SD card support, OLED, dynamic scenes, emergency stop, EEPROM
* NeoPixel status + per-button/relay indicators with editable mapping tables
* Using only visible pins from ESP32-S3 WROOM-1
*/
#include <Arduino.h>
#include <EEPROM.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_NeoPixel.h>
#include <SD.h>
#include <FS.h>
#include <ArduinoJson.h>
// ==================== PIN CONFIGURATION ====================
// MUX control pins (only visible pins)
int s0 = 12; // GPIO12
int s1 = 14; // GPIO14
int s2 = 13; // GPIO13
int s3 = 21; // GPIO21
// MUX signal pins
int mux1SignalPin = 4; // GPIO4
int mux2SignalPin = 5; // GPIO5
// Relay outputs (8 relays using visible pins)
const int relayPins[8] = {6, 7, 15, 16, 17, 18, 8, 3};
// System pins
#define STATUS_LED_PIN 2 // Built-in LED
#define NEOPIXEL_PIN 1 // NeoPixel data pin (U0TXD - OK but may blip on boot)
#define NEOPIXEL_COUNT 30 // Total number of NeoPixels (0..29)
// OLED Display (I2C)
#define OLED_SDA 9 // I2C Data (visible pin)
#define OLED_SCL 10 // I2C Clock (visible pin)
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
// SD Card SPI pins (using visible ESP32-S3 pins)
#define SD_CS_PIN 47 // Chip Select (GPIO47)
#define SD_MOSI_PIN 35 // MOSI (GPIO35)
#define SD_MISO_PIN 37 // MISO (GPIO37)
#define SD_SCK_PIN 36 // Clock (GPIO36)
// ==================== SYSTEM CONFIG ====================
#define EEPROM_SIZE 512
#define EEPROM_SETTINGS_ADDR 0
#define BUTTON_DEBOUNCE_MS 200
#define DISPLAY_UPDATE_MS 250
#define AUTO_SHUTOFF_MS 600000
#define BUTTON_THRESHOLD 2000
// ==================== GLOBALS ====================
struct SystemState {
bool relayStates[8] = {false};
uint32_t relayOnTime[8] = {0};
bool emergencyActive = false;
bool showRunning = false;
int8_t currentShow = -1;
uint32_t showStartTime = 0;
uint32_t lastActivity = 0;
uint16_t showsTriggered = 0;
// Button system
bool buttonStates[32] = {false};
bool lastButtonStates[32] = {false};
uint32_t lastButtonPress[32] = {0};
// Scene selection system
char sceneDigits[4] = {0, 0, 0, 0};
uint8_t digitIndex = 0;
uint32_t lastDigitTime = 0;
bool sceneReady = false;
// Scene Builder / Queue system
int sceneQueue[10] = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1};
uint8_t queueCount = 0;
uint8_t queueIndex = 0;
bool queueMode = false;
bool builderMode = false;
int builderCursor = 0;
uint32_t lastBuilderActivity = 0;
} sysState;
struct ShowSettings {
uint16_t magic = 0xABCD;
bool autoShutoff = true;
uint8_t maxShowDuration = 5; // minutes (not actively enforced yet)
bool enableSafetyLimits = true;
} settings;
// Show system - original structure for built-in shows
struct ShowStep {
uint16_t duration;
uint8_t relayMask;
const char* action;
};
struct Show {
const char* name;
ShowStep* steps;
uint8_t stepCount;
uint16_t totalDuration;
};
// Dynamic show system for SD card shows
struct DynamicShow {
String name;
ShowStep* steps;
uint8_t stepCount;
uint16_t totalDuration;
bool fromSD; // Track if show is from SD card
};
// SD Card globals
bool sdCardAvailable = false;
DynamicShow* dynamicShowLibrary = nullptr;
uint8_t dynamicShowCount = 0;
const uint8_t MAX_DYNAMIC_SHOWS = 100; // Limit for memory management
// ==================== SHOW LIBRARY (35 BUILT-IN SCENES) ====================
ShowStep scene001Steps[] = {{3000, 0b00000001, "Relay 1 Only"}, {1000, 0b00000000, "Off"}};
ShowStep scene002Steps[] = {{3000, 0b00000010, "Relay 2 Only"}, {1000, 0b00000000, "Off"}};
ShowStep scene003Steps[] = {{3000, 0b00000100, "Relay 3 Only"}, {1000, 0b00000000, "Off"}};
ShowStep scene004Steps[] = {{3000, 0b00001000, "Relay 4 Only"}, {1000, 0b00000000, "Off"}};
ShowStep scene005Steps[] = {{3000, 0b00010000, "Relay 5 Only"}, {1000, 0b00000000, "Off"}};
ShowStep scene006Steps[] = {{3000, 0b00100000, "Relay 6 Only"}, {1000, 0b00000000, "Off"}};
ShowStep scene007Steps[] = {{3000, 0b01000000, "Relay 7 Only"}, {1000, 0b00000000, "Off"}};
ShowStep scene008Steps[] = {{3000, 0b10000000, "Relay 8 Only"}, {1000, 0b00000000, "Off"}};
ShowStep scene009Steps[] = {{4000, 0b11111111, "All Relays On"}, {1000, 0b00000000, "Off"}};
ShowStep scene010Steps[] = {{2000, 0b00000011, "Relays 1&2"}, {1000, 0b00000000, "Off"}};
ShowStep scene011Steps[] = {{2000, 0b00000101, "Alternating 1"}, {1000, 0b00000000, "Off"}};
ShowStep scene012Steps[] = {{2000, 0b00001010, "Alternating 2"}, {1000, 0b00000000, "Off"}};
ShowStep scene013Steps[] = {{2000, 0b00010100, "Skip Pattern 1"}, {1000, 0b00000000, "Off"}};
ShowStep scene014Steps[] = {{2000, 0b00101000, "Skip Pattern 2"}, {1000, 0b00000000, "Off"}};
ShowStep scene015Steps[] = {{2000, 0b01010000, "High Alternating"}, {1000, 0b00000000, "Off"}};
ShowStep scene016Steps[] = {{2000, 0b10100000, "High Skip"}, {1000, 0b00000000, "Off"}};
ShowStep scene017Steps[] = {{1500, 0b00000001, "Flash 1"}, {500, 0b00000010, "Flash 2"}, {1000, 0b00000000, "Off"}};
ShowStep scene018Steps[] = {{1500, 0b00000100, "Flash 3"}, {500, 0b00001000, "Flash 4"}, {1000, 0b00000000, "Off"}};
ShowStep scene019Steps[] = {{1500, 0b00010000, "Flash 5"}, {500, 0b00100000, "Flash 6"}, {1000, 0b00000000, "Off"}};
ShowStep scene020Steps[] = {{1500, 0b01000000, "Flash 7"}, {500, 0b10000000, "Flash 8"}, {1000, 0b00000000, "Off"}};
ShowStep scene021Steps[] = {{2500, 0b00001111, "Low Four"}, {1000, 0b00000000, "Off"}};
ShowStep scene022Steps[] = {{2500, 0b11110000, "High Four"}, {1000, 0b00000000, "Off"}};
ShowStep scene023Steps[] = {{1000, 0b01010101, "Chess 1"}, {1000, 0b10101010, "Chess 2"}, {1000, 0b00000000, "Off"}};
ShowStep scene024Steps[] = {{3000, 0b11001100, "Center Pairs"}, {1000, 0b00000000, "Off"}};
ShowStep scene025Steps[] = {{3000, 0b00110011, "Edge Pairs"}, {1000, 0b00000000, "Off"}};
ShowStep scene026Steps[] = {{2500, 0b10011001, "Diagonal 1"}, {1000, 0b00000000, "Off"}};
ShowStep scene027Steps[] = {{2500, 0b01100110, "Diagonal 2"}, {1000, 0b00000000, "Off"}};
ShowStep scene028Steps[] = {{3000, 0b00011000, "Center Only"}, {1000, 0b00000000, "Off"}};
ShowStep scene029Steps[] = {{3000, 0b11000011, "Edges Only"}, {1000, 0b00000000, "Off"}};
ShowStep scene030Steps[] = {{3500, 0b00111100, "Inner Block"}, {1000, 0b00000000, "Off"}};
ShowStep scene031Steps[] = {{3500, 0b11000011, "Outer Block"}, {1000, 0b00000000, "Off"}};
ShowStep scene032Steps[] = {{4000, 0b01111110, "Almost All"}, {1000, 0b00000000, "Off"}};
ShowStep scene033Steps[] = {{4000, 0b10000001, "Bookends"}, {1000, 0b00000000, "Off"}};
ShowStep scene034Steps[] = {{5000, 0b11111110, "All But Last"}, {1000, 0b00000000, "Off"}};
ShowStep scene035Steps[] = {{5000, 0b01111111, "All But First"}, {1000, 0b00000000, "Off"}};
Show showLibrary[] = {
{"Scene 001", scene001Steps, 2, 0}, {"Scene 002", scene002Steps, 2, 0},
{"Scene 003", scene003Steps, 2, 0}, {"Scene 004", scene004Steps, 2, 0},
{"Scene 005", scene005Steps, 2, 0}, {"Scene 006", scene006Steps, 2, 0},
{"Scene 007", scene007Steps, 2, 0}, {"Scene 008", scene008Steps, 2, 0},
{"Scene 009", scene009Steps, 2, 0}, {"Scene 010", scene010Steps, 2, 0},
{"Scene 011", scene011Steps, 2, 0}, {"Scene 012", scene012Steps, 2, 0},
{"Scene 013", scene013Steps, 2, 0}, {"Scene 014", scene014Steps, 2, 0},
{"Scene 015", scene015Steps, 2, 0}, {"Scene 016", scene016Steps, 2, 0},
{"Scene 017", scene017Steps, 3, 0}, {"Scene 018", scene018Steps, 3, 0},
{"Scene 019", scene019Steps, 3, 0}, {"Scene 020", scene020Steps, 3, 0},
{"Scene 021", scene021Steps, 2, 0}, {"Scene 022", scene022Steps, 2, 0},
{"Scene 023", scene023Steps, 3, 0}, {"Scene 024", scene024Steps, 2, 0},
{"Scene 025", scene025Steps, 2, 0}, {"Scene 026", scene026Steps, 2, 0},
{"Scene 027", scene027Steps, 2, 0}, {"Scene 028", scene028Steps, 2, 0},
{"Scene 029", scene029Steps, 2, 0}, {"Scene 030", scene030Steps, 2, 0},
{"Scene 031", scene031Steps, 2, 0}, {"Scene 032", scene032Steps, 2, 0},
{"Scene 033", scene033Steps, 2, 0}, {"Scene 034", scene034Steps, 2, 0},
{"Scene 035", scene035Steps, 2, 0},
};
#define SHOW_COUNT (sizeof(showLibrary)/sizeof(Show))
// ==================== OLED + NeoPixel Instances ====================
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
bool oledAvailable = false;
Adafruit_NeoPixel strip(NEOPIXEL_COUNT, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
// ==================== Colors ====================
#define COLOR_OFF 0x000000
#define COLOR_GREEN 0x00FF00
#define COLOR_RED 0xFF0000
#define COLOR_BLUE 0x0000FF
#define COLOR_YELLOW 0xFFFF00
#define COLOR_PURPLE 0xFF00FF
#define COLOR_CYAN 0x00FFFF
#define COLOR_WHITE 0xFFFFFF
#define COLOR_ORANGE 0xFF8000
// ==================== NEO ROLE MAPPING ====================
// --- System indicators ---
constexpr int NEO_IDX_POWER = 0; // Power / On indicator
constexpr int NEO_IDX_WORKING = 1; // Working / heartbeat
// --- Control buttons ---
constexpr int NEO_IDX_RESET = 12; // Reset button LED index
constexpr int NEO_IDX_START = 13; // Start button LED index
// --- Buttons (0 → 9) ---
int NEO_INDEX_BUTTON[10] = {
/*Button 0*/ 11,
/*Button 1*/ 4,
/*Button 2*/ 3,
/*Button 3*/ 2,
/*Button 4*/ 7,
/*Button 5*/ 6,
/*Button 6*/ 5,
/*Button 7*/ 10,
/*Button 8*/ 9,
/*Button 9*/ 8
};
// --- Relays (1 → 8) ---
int NEO_INDEX_RELAY[8] = {
/*Relay 1*/ 14,
/*Relay 2*/ 15,
/*Relay 3*/ 16,
/*Relay 4*/ 17,
/*Relay 5*/ 18,
/*Relay 6*/ 19,
/*Relay 7*/ 20,
/*Relay 8*/ 21
};
// ==================== NeoPixel helpers/prototypes ====================
static inline void setPixelHex(int idx, uint32_t hex) {
if (idx < 0 || idx >= NEOPIXEL_COUNT) return;
uint8_t r = (hex >> 16) & 0xFF;
uint8_t g = (hex >> 8) & 0xFF;
uint8_t b = (hex ) & 0xFF;
strip.setPixelColor(idx, r, g, b);
}
uint32_t btnFlashAt[10] = {0};
bool workBlink = false;
uint32_t lastWorkBlink = 0;
void initNeoPixels();
void updateAllNeoPixels();
void showButtonPress(int buttonNum);
void pixelWalkTest(uint16_t holdMs = 250);
// ==================== SD CARD FUNCTIONS ====================
bool initSDCard() {
Serial.println("Initializing SD Card...");
// Initialize SPI for SD card
SPI.begin(SD_SCK_PIN, SD_MISO_PIN, SD_MOSI_PIN, SD_CS_PIN);
if (!SD.begin(SD_CS_PIN)) {
Serial.println("SD Card initialization failed!");
return false;
}
uint8_t cardType = SD.cardType();
if (cardType == CARD_NONE) {
Serial.println("No SD card attached");
return false;
}
Serial.print("SD Card Type: ");
switch(cardType) {
case CARD_MMC: Serial.println("MMC"); break;
case CARD_SD: Serial.println("SDSC"); break;
case CARD_SDHC: Serial.println("SDHC"); break;
default: Serial.println("UNKNOWN"); break;
}
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf("SD Card Size: %lluMB\n", cardSize);
return true;
}
void createDefaultShowFiles() {
Serial.println("Creating default show files...");
// Create shows directory
if (!SD.exists("/shows")) {
SD.mkdir("/shows");
}
// Create example show file
File exampleFile = SD.open("/shows/example.json", FILE_WRITE);
if (exampleFile) {
exampleFile.println("{");
exampleFile.println(" \"name\": \"Example Show\",");
exampleFile.println(" \"description\": \"A simple example show\",");
exampleFile.println(" \"steps\": [");
exampleFile.println(" {");
exampleFile.println(" \"duration\": 2000,");
exampleFile.println(" \"relayMask\": \"0b00000001\",");
exampleFile.println(" \"action\": \"Turn on Relay 1\"");
exampleFile.println(" },");
exampleFile.println(" {");
exampleFile.println(" \"duration\": 1000,");
exampleFile.println(" \"relayMask\": \"0b00000010\",");
exampleFile.println(" \"action\": \"Turn on Relay 2\"");
exampleFile.println(" },");
exampleFile.println(" {");
exampleFile.println(" \"duration\": 500,");
exampleFile.println(" \"relayMask\": \"0b00000000\",");
exampleFile.println(" \"action\": \"All Off\"");
exampleFile.println(" }");
exampleFile.println(" ]");
exampleFile.println("}");
exampleFile.close();
Serial.println("Created example.json");
}
// Create README file
File readmeFile = SD.open("/shows/README.txt", FILE_WRITE);
if (readmeFile) {
readmeFile.println("SHOWDUINO SD CARD SHOWS");
readmeFile.println("======================");
readmeFile.println("");
readmeFile.println("Place JSON show files in this directory.");
readmeFile.println("Files should follow this format:");
readmeFile.println("");
readmeFile.println("{");
readmeFile.println(" \"name\": \"Show Name\",");
readmeFile.println(" \"description\": \"Optional description\",");
readmeFile.println(" \"steps\": [");
readmeFile.println(" {");
readmeFile.println(" \"duration\": 2000,");
readmeFile.println(" \"relayMask\": \"0b11111111\",");
readmeFile.println(" \"action\": \"Description\"");
readmeFile.println(" }");
readmeFile.println(" ]");
readmeFile.println("}");
readmeFile.println("");
readmeFile.println("relayMask: 8-bit binary (0b00000000 to 0b11111111)");
readmeFile.println("duration: milliseconds");
readmeFile.close();
Serial.println("Created README.txt");
}
}
uint8_t parseBinaryString(const char* binaryStr) {
if (strncmp(binaryStr, "0b", 2) == 0) {
return (uint8_t)strtol(binaryStr + 2, NULL, 2);
}
return (uint8_t)strtol(binaryStr, NULL, 10);
}
bool loadShowFromSD(const char* filename, DynamicShow* show) {
String fullPath = "/shows/" + String(filename);
File file = SD.open(fullPath);
if (!file) {
Serial.printf("Failed to open: %s\n", fullPath.c_str());
return false;
}
String jsonContent = file.readString();
file.close();
// Parse JSON
DynamicJsonDocument doc(4096); // Adjust size as needed
DeserializationError error = deserializeJson(doc, jsonContent);
if (error) {
Serial.printf("JSON parse error in %s: %s\n", filename, error.c_str());
return false;
}
// Extract show data
show->name = doc["name"].as<String>();
show->fromSD = true;
JsonArray stepsArray = doc["steps"];
show->stepCount = stepsArray.size();
if (show->stepCount == 0) {
Serial.printf("No steps found in %s\n", filename);
return false;
}
// Allocate memory for steps
show->steps = (ShowStep*)malloc(sizeof(ShowStep) * show->stepCount);
if (!show->steps) {
Serial.println("Failed to allocate memory for steps");
return false;
}
// Parse steps
uint16_t totalDuration = 0;
for (uint8_t i = 0; i < show->stepCount; i++) {
JsonObject step = stepsArray[i];
show->steps[i].duration = step["duration"];
show->steps[i].relayMask = parseBinaryString(step["relayMask"]);
// Copy action string (limit to prevent memory issues)
String action = step["action"].as<String>();
if (action.length() > 50) action = action.substring(0, 50);
// Allocate and copy action string
show->steps[i].action = strdup(action.c_str());
totalDuration += show->steps[i].duration;
}
show->totalDuration = totalDuration;
Serial.printf("Loaded show: %s (%d steps, %dms total)\n",
show->name.c_str(), show->stepCount, show->totalDuration);
return true;
}
void loadAllSDShows() {
if (!sdCardAvailable) return;
Serial.println("Loading shows from SD card...");
// Free existing dynamic shows
if (dynamicShowLibrary) {
for (int i = 0; i < dynamicShowCount; i++) {
if (dynamicShowLibrary[i].fromSD && dynamicShowLibrary[i].steps) {
for (int j = 0; j < dynamicShowLibrary[i].stepCount; j++) {
free((void*)dynamicShowLibrary[i].steps[j].action);
}
free(dynamicShowLibrary[i].steps);
}
}
free(dynamicShowLibrary);
}
// Initialize dynamic show library (includes built-in + SD shows)
dynamicShowLibrary = (DynamicShow*)malloc(sizeof(DynamicShow) * MAX_DYNAMIC_SHOWS);
if (!dynamicShowLibrary) {
Serial.println("Failed to allocate memory for dynamic shows");
return;
}
dynamicShowCount = 0;
// First, copy built-in shows (scenes 1-35)
for (int i = 0; i < SHOW_COUNT && dynamicShowCount < MAX_DYNAMIC_SHOWS; i++) {
dynamicShowLibrary[dynamicShowCount].name = String(showLibrary[i].name);
dynamicShowLibrary[dynamicShowCount].steps = showLibrary[i].steps;
dynamicShowLibrary[dynamicShowCount].stepCount = showLibrary[i].stepCount;
dynamicShowLibrary[dynamicShowCount].totalDuration = showLibrary[i].totalDuration;
dynamicShowLibrary[dynamicShowCount].fromSD = false;
dynamicShowCount++;
}
// Then load SD card shows
File root = SD.open("/shows");
if (!root || !root.isDirectory()) {
Serial.println("Shows directory not found");
return;
}
File file = root.openNextFile();
while (file && dynamicShowCount < MAX_DYNAMIC_SHOWS) {
if (!file.isDirectory()) {
String filename = file.name();
if (filename.endsWith(".json")) {
if (loadShowFromSD(filename.c_str(), &dynamicShowLibrary[dynamicShowCount])) {
dynamicShowCount++;
}
}
}
file = root.openNextFile();
}
root.close();
Serial.printf("Loaded %d total shows (%d built-in + %d from SD)\n",
dynamicShowCount, SHOW_COUNT, dynamicShowCount - SHOW_COUNT);
}
void listSDShows() {
if (!sdCardAvailable) {
Serial.println("SD card not available");
return;
}
Serial.println("\n=== SD CARD SHOWS ===");
File root = SD.open("/shows");
if (!root) {
Serial.println("Failed to open shows directory");
return;
}
int count = 0;
File file = root.openNextFile();
while (file) {
if (!file.isDirectory() && String(file.name()).endsWith(".json")) {
Serial.printf("%3d. %s (%d bytes)\n", ++count, file.name(), file.size());
}
file = root.openNextFile();
}
root.close();
if (count == 0) {
Serial.println("No JSON show files found");
}
Serial.println("==================\n");
}
// ==================== MULTIPLEXER FUNCTIONS ====================
int readMux(int muxNumber, int channel) {
int controlPin[] = {s0, s1, s2, s3};
int muxChannel[16][4] = {
{0,0,0,0}, {1,0,0,0}, {0,1,0,0}, {1,1,0,0},
{0,0,1,0}, {1,0,1,0}, {0,1,1,0}, {1,1,1,0},
{0,0,0,1}, {1,0,0,1}, {0,1,0,1}, {1,1,0,1},
{0,0,1,1}, {1,0,1,1}, {0,1,1,1}, {1,1,1,1}
};
// Set channel
for (int i = 0; i < 4; i++) {
digitalWrite(controlPin[i], muxChannel[channel][i]);
}
delayMicroseconds(50);
// Read from appropriate signal pin
int signalPin = (muxNumber == 1) ? mux1SignalPin : mux2SignalPin;
return analogRead(signalPin);
}
bool isButtonPressed(int buttonNum) {
int muxNumber = (buttonNum < 16) ? 1 : 2;
int channel = buttonNum % 16;
return (readMux(muxNumber, channel) > BUTTON_THRESHOLD);
}
void readButtons() {
for (int i = 0; i < 32; i++) {
sysState.lastButtonStates[i] = sysState.buttonStates[i];
sysState.buttonStates[i] = isButtonPressed(i);
}
}
// ==================== RELAY CONTROL ====================
void setRelay(int idx, bool state) {
if (idx >= 0 && idx < 8) {
digitalWrite(relayPins[idx], state ? HIGH : LOW);
sysState.relayStates[idx] = state;
if (state) sysState.relayOnTime[idx] = millis();
}
}
void toggleRelay(int relayIndex) {
bool newState = !sysState.relayStates[relayIndex];
setRelay(relayIndex, newState);
Serial.printf("Manual relay %d: %s\n", relayIndex + 1, newState ? "ON" : "OFF");
}
// ==================== SHOW CONTROL (UPDATED FOR DYNAMIC SHOWS) ====================
void triggerDynamicShow(int idx) {
if (idx >= dynamicShowCount) return;
if (sysState.showRunning) stopShow();
sysState.currentShow = idx;
sysState.showRunning = true;
sysState.showStartTime = millis();
sysState.showsTriggered++;
// Execute first step
DynamicShow* show = &dynamicShowLibrary[idx];
for (int i = 0; i < 8; i++) {
bool on = (show->steps[0].relayMask >> i) & 1;
setRelay(i, on);
}
Serial.printf("Dynamic show started: %s %s\n", show->name.c_str(), show->fromSD ? "(SD)" : "");
}
void executeDynamicShowStep(int stepIndex) {
DynamicShow* show = &dynamicShowLibrary[sysState.currentShow];
ShowStep* step = &show->steps[stepIndex];
for (int i = 0; i < 8; i++) {
bool on = (step->relayMask >> i) & 1;
setRelay(i, on);
}
Serial.printf("Step %d: %s\n", stepIndex, step->action);
}
void updateDynamicShow() {
static int currentStep = 0;
static uint32_t stepStart = 0;
if (stepStart == 0) stepStart = millis();
DynamicShow* show = &dynamicShowLibrary[sysState.currentShow];
if (millis() - stepStart >= show->steps[currentStep].duration) {
currentStep++;
if (currentStep < show->stepCount) {
executeDynamicShowStep(currentStep);
stepStart = millis();
} else {
stopShow();
currentStep = 0;
stepStart = 0;
updateQueue();
}
}
}
void stopShow() {
for (int i = 0; i < 8; i++) setRelay(i, false);
sysState.showRunning = false;
sysState.currentShow = -1;
Serial.println("Show stopped.");
}
// ==================== SCENE SELECTION ====================
void handleSceneEntry(int digit) {
Serial.printf("DEBUG: Builder mode = %s, entering digit %d\n",
sysState.builderMode ? "ON" : "OFF", digit);
if (sysState.digitIndex < 3) {
sysState.sceneDigits[sysState.digitIndex] = digit;
sysState.digitIndex++;
sysState.lastDigitTime = millis();
Serial.printf("Scene entry: ");
for (int i = 0; i < sysState.digitIndex; i++) Serial.printf("%d", sysState.sceneDigits[i]);
for (int i = sysState.digitIndex; i < 3; i++) Serial.printf("_");
Serial.printf(" (lastDigitTime=%lu)\n", sysState.lastDigitTime);
if (sysState.digitIndex == 3) {
sysState.sceneReady = true;
Serial.printf("Scene %03d ready - press START\n", (sysState.sceneDigits[0] * 100) + (sysState.sceneDigits[1] * 10) + sysState.sceneDigits[2]);
}
}
}
int getSceneNumber() {
return (sysState.sceneDigits[0] * 100) + (sysState.sceneDigits[1] * 10) + sysState.sceneDigits[2];
}
void executeSceneSelection() {
if (sysState.sceneReady) {
int sceneNum = getSceneNumber();
if (sceneNum >= 1 && sceneNum <= dynamicShowCount) {
if (sysState.builderMode) {
// Add to queue instead of playing immediately
addToQueue(sceneNum - 1);
Serial.println("Scene added! Enter next scene or press SELECT to start queue.");
} else {
triggerDynamicShow(sceneNum - 1);
}
// Reset scene entry
sysState.digitIndex = 0;
sysState.sceneReady = false;
memset(sysState.sceneDigits, 0, sizeof(sysState.sceneDigits));
} else {
Serial.printf("Invalid scene: %03d (max: %d)\n", sceneNum, dynamicShowCount);
sysState.digitIndex = 0;
sysState.sceneReady = false;
memset(sysState.sceneDigits, 0, sizeof(sysState.sceneDigits));
}
}
}
// ==================== SCENE BUILDER / QUEUE SYSTEM ====================
void addToQueue(int sceneIndex) {
if (sysState.queueCount < 10) {
sysState.sceneQueue[sysState.queueCount] = sceneIndex;
sysState.queueCount++;
Serial.printf("Added scene %03d to queue (position %d)\n", sceneIndex + 1, sysState.queueCount);
} else {
Serial.println("Queue full! Cannot add more scenes.");
}
}
void clearQueue() {
for (int i = 0; i < 10; i++) {
sysState.sceneQueue[i] = -1;
}
sysState.queueCount = 0;
sysState.queueIndex = 0;
sysState.queueMode = false;
Serial.println("Queue cleared.");
}
void startQueue() {
if (sysState.queueCount > 0) {
sysState.queueMode = true;
sysState.queueIndex = 0;
triggerDynamicShow(sysState.sceneQueue[0]);
Serial.printf("Starting queue with %d scenes\n", sysState.queueCount);
} else {
Serial.println("Queue is empty!");
}
}
void updateQueue() {
if (sysState.queueMode && !sysState.showRunning) {
// Current show finished, move to next in queue
sysState.queueIndex++;
if (sysState.queueIndex < sysState.queueCount) {
triggerDynamicShow(sysState.sceneQueue[sysState.queueIndex]);
Serial.printf("Queue: Starting scene %d of %d\n", sysState.queueIndex + 1, sysState.queueCount);
} else {
// Queue finished
Serial.println("Queue completed!");
sysState.queueMode = false;
sysState.queueIndex = 0;
}
}
}
void toggleBuilderMode() {
sysState.builderMode = !sysState.builderMode;
sysState.lastBuilderActivity = millis();
if (sysState.builderMode) {
Serial.println("=== SCENE BUILDER MODE ACTIVATED ===");
Serial.println("Enter scenes to add to queue, then press SELECT to start");
} else {
Serial.println("Scene Builder mode deactivated");
}
}
void navigateQueue(bool up) {
if (sysState.queueCount > 0) {
if (up) {
sysState.builderCursor = (sysState.builderCursor > 0) ? sysState.builderCursor - 1 : sysState.queueCount - 1;
} else {
sysState.builderCursor = (sysState.builderCursor < sysState.queueCount - 1) ? sysState.builderCursor + 1 : 0;
}
Serial.printf("Queue cursor: %d (Scene %03d)\n", sysState.builderCursor, sysState.sceneQueue[sysState.builderCursor] + 1);
}
}
// ==================== BUTTON HANDLING ====================
void handleButtonPresses() {
uint32_t currentTime = millis();
for (int i = 0; i < 32; i++) {
if (sysState.buttonStates[i] && !sysState.lastButtonStates[i]) {
if (currentTime - sysState.lastButtonPress[i] > BUTTON_DEBOUNCE_MS) {
sysState.lastButtonPress[i] = currentTime;
sysState.lastActivity = currentTime;
Serial.printf("Button %d pressed\n", i);
// Visual ping on NeoPixel for digit buttons
showButtonPress(i);
// Digits 0..9
if (i >= 0 && i <= 9) {
handleSceneEntry(i);
}
// EMERGENCY STOP
else if (i == 31) {
Serial.println("=== EMERGENCY STOP ACTIVATED ===");
sysState.emergencyActive = true;
stopShow();
for (int j = 0; j < 8; j++) setRelay(j, false);
}
// START/ACCEPT
else if (i == 16) {
Serial.println("START button detected");
if (sysState.emergencyActive) {
sysState.emergencyActive = false;
digitalWrite(STATUS_LED_PIN, LOW);
Serial.println("Emergency cleared via START");
} else {
executeSceneSelection();
}
}
// RESET
else if (i == 17) {
Serial.println("RESET button detected");
if (sysState.emergencyActive) {
sysState.emergencyActive = false;
digitalWrite(STATUS_LED_PIN, LOW);
Serial.println("Emergency cleared via RESET");
} else {
sysState.digitIndex = 0;
sysState.sceneReady = false;
memset(sysState.sceneDigits, 0, sizeof(sysState.sceneDigits));
clearQueue();
sysState.builderMode = false;
Serial.println("Scene entry and queue reset");
}
}
// Relay toggles (buttons 18..25)
else if (i >= 18 && i <= 25) {
int relayIdx = i - 18; // Button 18 = Relay 0, etc.
Serial.printf("Relay toggle button %d (relay %d)\n", i, relayIdx + 1);
if (relayIdx < 8) {
toggleRelay(relayIdx);
}
}
// Scene builder buttons on MUX2 channels 10–13 (button indices 26-29)
else if (i == 26) {
// QUEUE button (MUX2 channel 10) - Toggle builder mode
toggleBuilderMode();
}
else if (i == 27) {
// UP button (MUX2 channel 11) - Navigate queue up
if (sysState.builderMode) {
navigateQueue(true);
} else {
Serial.println("UP: Enter builder mode first (press QUEUE)");
}
}
else if (i == 28) {
// DOWN button (MUX2 channel 12) - Navigate queue down
if (sysState.builderMode) {
navigateQueue(false);
} else {
Serial.println("DOWN: Enter builder mode first (press QUEUE)");
}
}
else if (i == 29) {
// SELECT button (MUX2 channel 13) - Start queue or clear
if (sysState.builderMode) {
if (sysState.queueCount > 0) {
startQueue();
sysState.builderMode = false;
} else {
Serial.println("Queue is empty! Add scenes first.");
}
} else {
clearQueue();
}
}
// RELOAD SD button (MUX2 channel 14) - Button 30
else if (i == 30) {
Serial.println("RELOAD SD button pressed");
loadAllSDShows();
listSDShows();
}
else {
Serial.printf("Unmapped button %d pressed\n", i);
}
}
}
}
}
// ==================== EMERGENCY SYSTEM ====================
void handleEmergencyMode() {
if (sysState.emergencyActive) {
static uint32_t lastFlash = 0;
if (millis() - lastFlash > 500) {
digitalWrite(STATUS_LED_PIN, !digitalRead(STATUS_LED_PIN));
lastFlash = millis();
}
// Ensure all relays off
for (int i = 0; i < 8; i++) setRelay(i, false);
} else {
digitalWrite(STATUS_LED_PIN, LOW);
}
}
void handleEmergencyButtons() {
uint32_t currentTime = millis();
for (int i = 16; i <= 17; i++) {
if (sysState.buttonStates[i] && !sysState.lastButtonStates[i]) {
if (currentTime - sysState.lastButtonPress[i] > BUTTON_DEBOUNCE_MS) {
sysState.lastButtonPress[i] = currentTime;
Serial.printf("Emergency mode: Button %d pressed\n", i);
sysState.emergencyActive = false;
digitalWrite(STATUS_LED_PIN, LOW);
Serial.println("Emergency cleared");
}
}
}
}
// ==================== DISPLAY SYSTEMS (UPDATED FOR SD SUPPORT) ====================
void updateDisplay() {
if (sysState.emergencyActive) {
Serial.println("[DISPLAY] ⚠️ EMERGENCY STOP ACTIVE - Press START/RESET to clear");
} else if (sysState.showRunning) {
DynamicShow* currentShow = &dynamicShowLibrary[sysState.currentShow];
if (sysState.queueMode) {
Serial.printf("[DISPLAY] Queue %d/%d: %s %s\n",
sysState.queueIndex + 1, sysState.queueCount,
currentShow->name.c_str(),
currentShow->fromSD ? "(SD)" : "");
} else {
Serial.printf("[DISPLAY] Running: %s %s\n",
currentShow->name.c_str(),
currentShow->fromSD ? "(SD)" : "");
}
} else if (sysState.builderMode) {
Serial.printf("[DISPLAY] BUILDER MODE | Queue: %d scenes", sysState.queueCount);
if (sysState.queueCount > 0) {
int sceneIdx = sysState.sceneQueue[sysState.builderCursor];
if (sceneIdx < dynamicShowCount) {
Serial.printf(" | Cursor: %s", dynamicShowLibrary[sceneIdx].name.c_str());
}
}
Serial.println();
} else if (sysState.sceneReady) {
int sceneNum = getSceneNumber();
if (sceneNum <= dynamicShowCount) {
Serial.printf("[DISPLAY] Ready: %s - press START\n",
dynamicShowLibrary[sceneNum - 1].name.c_str());
} else {
Serial.printf("[DISPLAY] Scene %03d ready - press START\n", sceneNum);
}
} else if (sysState.digitIndex > 0) {
Serial.printf("[DISPLAY] Entering scene: ");
for (int i = 0; i < sysState.digitIndex; i++) Serial.printf("%d", sysState.sceneDigits[i]);
for (int i = sysState.digitIndex; i < 3; i++) Serial.printf("_");
Serial.println();
} else {
Serial.printf("[DISPLAY] Ready | Shows: %d (%d SD) | Triggered: %d",
dynamicShowCount,
sdCardAvailable ? dynamicShowCount - SHOW_COUNT : 0,
sysState.showsTriggered);
if (sysState.queueCount > 0) {
Serial.printf(" | Queue: %d scenes", sysState.queueCount);
}
Serial.println();
}
}
void initOLED() {
Serial.println("Initializing OLED...");
Wire.begin(OLED_SDA, OLED_SCL);
if (display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
oledAvailable = true;
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println(F("SHOWDUINO v2.7"));
display.println(F("Professional"));
display.println(F("Show Controller"));
display.println();
display.printf("SD Card: %s", sdCardAvailable ? "Ready" : "None");
display.display();
Serial.println("OLED initialized successfully!");
} else {
oledAvailable = false;
Serial.println("OLED not found, continuing without display");
}
}
void updateOLEDDisplay() {
if (!oledAvailable) return;
display.clearDisplay();
display.setCursor(0, 0);
display.setTextSize(1);
display.println(F("SHOWDUINO v2.7"));
display.drawLine(0, 10, 127, 10, SSD1306_WHITE);
if (sysState.emergencyActive) {
display.setTextSize(2);
display.setCursor(10, 20);
display.println(F("EMERGENCY"));
display.setCursor(30, 40);
display.println(F("STOP"));
}
else if (sysState.showRunning) {
DynamicShow* currentShow = &dynamicShowLibrary[sysState.currentShow];
if (sysState.queueMode) {
display.setCursor(0, 15);
display.printf("QUEUE %d/%d:", sysState.queueIndex + 1, sysState.queueCount);
display.setCursor(0, 25);
display.println(currentShow->name.c_str());
if (currentShow->fromSD) {
display.setCursor(0, 35);
display.println(F("(SD Card)"));
}
} else {
display.setCursor(0, 15);
display.println(F("RUNNING:"));
display.setCursor(0, 25);
display.println(currentShow->name.c_str());
if (currentShow->fromSD) {
display.setCursor(0, 35);
display.println(F("(SD Card)"));
}
}
display.setCursor(0, 50);
display.print(F("Relays: "));
for (int i = 0; i < 8; i++) display.print(sysState.relayStates[i] ? "1" : "0");
}
else if (sysState.builderMode) {
display.setCursor(0, 15);
display.println(F("SCENE BUILDER"));
if (sysState.digitIndex > 0) {
// Show partial scene entry (digits being typed)
display.setCursor(0, 28);
display.println(F("Enter Scene:"));
display.setTextSize(3);
display.setCursor(15, 40);
for (int i = 0; i < sysState.digitIndex; i++) display.printf("%d", sysState.sceneDigits[i]);
for (int i = sysState.digitIndex; i < 3; i++) display.print("_");
display.setTextSize(1);
} else {
// No digits in progress, so show queue info
display.setCursor(0, 25);
display.printf("Queue: %d scenes", sysState.queueCount);
if (sysState.queueCount > 0) {
display.setCursor(0, 35);
display.printf("Cursor: %s", dynamicShowLibrary[sysState.sceneQueue[sysState.builderCursor]].name.c_str());
}
display.setCursor(0, 50);
display.println(F("UP/DOWN/SELECT"));
}
}
else if (sysState.sceneReady) {
display.setCursor(0, 15);
display.println(F("SCENE READY:"));
display.setTextSize(3);
display.setCursor(25, 28);
display.printf("%03d", getSceneNumber());
display.setTextSize(1);
display.setCursor(10, 52);
display.println(F("Press START"));
}
else if (sysState.digitIndex > 0) {
display.setCursor(0, 15);
display.println(F("Enter Scene:"));
display.setTextSize(3);
display.setCursor(15, 30);
for (int i = 0; i < sysState.digitIndex; i++) display.printf("%d", sysState.sceneDigits[i]);
for (int i = sysState.digitIndex; i < 3; i++) display.print("_");
}
else {
display.setCursor(0, 15);
display.println(F("READY"));
display.setCursor(0, 25);
display.printf("Shows: %d", dynamicShowCount);
if (sdCardAvailable) {
display.setCursor(0, 35);
display.printf("SD: %d shows", dynamicShowCount - SHOW_COUNT);
} else {
display.setCursor(0, 35);
display.println(F("Enter 3-digit scene"));
}
display.setCursor(0, 50);
if (sysState.queueCount > 0) {
display.printf("Queue: %d scenes", sysState.queueCount);
} else {
display.println(F("QUEUE for builder"));
}
}
display.display();
}
// ==================== UTILITIES ====================
void calculateShowDurations() {
for (int i = 0; i < SHOW_COUNT; i++) {
uint16_t total = 0;
for (int j = 0; j < showLibrary[i].stepCount; j++) {
total += showLibrary[i].steps[j].duration;
}
showLibrary[i].totalDuration = total;
}
}
void loadSettings() {
EEPROM.get(EEPROM_SETTINGS_ADDR, settings);
if (settings.magic != 0xABCD) {
settings = ShowSettings();
EEPROM.put(EEPROM_SETTINGS_ADDR, settings);
EEPROM.commit();
}
}
void performSafetyChecks() {
if (settings.autoShutoff && (millis() - sysState.lastActivity) > AUTO_SHUTOFF_MS) {
stopShow();
sysState.lastActivity = millis();
Serial.println("Auto-shutdown");
}
}
// ==================== NeoPixel IMPLEMENTATION ====================
void initNeoPixels() {
strip.begin();
strip.clear();
strip.setBrightness(64);
// Power + Working
setPixelHex(NEO_IDX_POWER, COLOR_GREEN);
setPixelHex(NEO_IDX_WORKING, COLOR_BLUE);
// Clear buttons & relays
for (int b = 0; b <= 9; b++) {
btnFlashAt[b] = 0;
int idx = NEO_INDEX_BUTTON[b];
if (idx >= 0) setPixelHex(idx, COLOR_OFF);
}
for (int r = 0; r < 8; r++) {
int idx = NEO_INDEX_RELAY[r];
if (idx >= 0) setPixelHex(idx, COLOR_OFF);
}
strip.show();
}
void showButtonPress(int buttonNum) {
if (buttonNum < 0 || buttonNum > 9) return;
int idx = NEO_INDEX_BUTTON[buttonNum];
if (idx < 0) return;
btnFlashAt[buttonNum] = millis(); // start flash window
setPixelHex(idx, COLOR_WHITE);
strip.show();
}
void updateAllNeoPixels() {
uint32_t now = millis();
// Working heartbeat (faster when a show runs)
uint16_t period = sysState.showRunning ? 250 : 500;
if (now - lastWorkBlink >= period) {
workBlink = !workBlink;
lastWorkBlink = now;
}
setPixelHex(NEO_IDX_WORKING, workBlink ? COLOR_BLUE : COLOR_OFF);
// Power indicator: green normal, red in emergency, cyan if SD available
uint32_t powerColor = COLOR_GREEN;
if (sysState.emergencyActive) {
powerColor = COLOR_RED;
} else if (sdCardAvailable) {
powerColor = COLOR_CYAN; // Cyan indicates SD card is available
}
setPixelHex(NEO_IDX_POWER, powerColor);
// Relay indicators follow live states
for (int r = 0; r < 8; r++) {
int idx = NEO_INDEX_RELAY[r];
if (idx >= 0) setPixelHex(idx, sysState.relayStates[r] ? COLOR_ORANGE : COLOR_OFF);
}
// Button flashes decay (200 ms)
for (int b = 0; b <= 9; b++) {
int idx = NEO_INDEX_BUTTON[b];
if (idx < 0) continue;
uint32_t t = btnFlashAt[b];
if (t != 0) {
if (now - t < 200) {
setPixelHex(idx, COLOR_WHITE);
} else {
btnFlashAt[b] = 0;
setPixelHex(idx, COLOR_OFF);
}
}
}
strip.show();
}
// Handy mapping verifier — run once, then adjust arrays above
void pixelWalkTest(uint16_t holdMs) {
Serial.println("\n=== NeoPixel Walk Test ===");
for (int i = 0; i < NEOPIXEL_COUNT; i++) {
strip.clear();
setPixelHex(i, COLOR_WHITE);
strip.show();
Serial.printf("Lit Neo index: %d\n", i);
delay(holdMs);
}
strip.clear();
setPixelHex(NEO_IDX_POWER, sdCardAvailable ? COLOR_CYAN : COLOR_GREEN);
setPixelHex(NEO_IDX_WORKING, COLOR_BLUE);
strip.show();
Serial.println("=== End NeoPixel Walk Test ===\n");
}
// ==================== SETUP ====================
void setup() {
Serial.begin(115200);
delay(2000);
Serial.println("=== SHOWDUINO ESP32-S3 PROFESSIONAL v2.7 ===");
Serial.println("Complete system with SD card support, OLED, dynamic scenes, emergency stop");
EEPROM.begin(EEPROM_SIZE);
loadSettings();
// Initialize SD card first
sdCardAvailable = initSDCard();
// Relays
Serial.println("Setting up relays...");
for (int i = 0; i < 8; i++) {
pinMode(relayPins[i], OUTPUT);
digitalWrite(relayPins[i], LOW);
}
// MUX control
Serial.println("Setting up multiplexer...");
pinMode(s0, OUTPUT);
pinMode(s1, OUTPUT);
pinMode(s2, OUTPUT);
pinMode(s3, OUTPUT);
digitalWrite(s0, LOW);
digitalWrite(s1, LOW);
digitalWrite(s2, LOW);
digitalWrite(s3, LOW);
// Status LED
pinMode(STATUS_LED_PIN, OUTPUT);
// OLED + NeoPixels (after SD init so display shows SD status)
initOLED();
initNeoPixels();
// SD Card setup
if (sdCardAvailable) {
createDefaultShowFiles();
loadAllSDShows();
listSDShows();
} else {
Serial.println("Continuing without SD card functionality");
// Still need to initialize dynamic library with built-in shows
dynamicShowLibrary = (DynamicShow*)malloc(sizeof(DynamicShow) * MAX_DYNAMIC_SHOWS);
if (dynamicShowLibrary) {
dynamicShowCount = 0;
for (int i = 0; i < SHOW_COUNT && dynamicShowCount < MAX_DYNAMIC_SHOWS; i++) {
dynamicShowLibrary[dynamicShowCount].name = String(showLibrary[i].name);
dynamicShowLibrary[dynamicShowCount].steps = showLibrary[i].steps;
dynamicShowLibrary[dynamicShowCount].stepCount = showLibrary[i].stepCount;
dynamicShowLibrary[dynamicShowCount].totalDuration = showLibrary[i].totalDuration;
dynamicShowLibrary[dynamicShowCount].fromSD = false;
dynamicShowCount++;
}
}
}
// (Optional) Pixel walk test — uncomment for mapping verification
// pixelWalkTest(100);
// Pre-calc durations
calculateShowDurations();
sysState.lastActivity = millis();
sysState.lastDigitTime = millis();
Serial.println("Configuration:");
Serial.printf("Control: s0=%d, s1=%d, s2=%d, s3=%d\n", s0, s1, s2, s3);
Serial.printf("Signals: MUX1=%d, MUX2=%d\n", mux1SignalPin, mux2SignalPin);
Serial.printf("Relays: 6,7,15,16,17,18,8,3\n");
Serial.printf("OLED: %s\n", oledAvailable ? "Available" : "Not found");
Serial.printf("NeoPixels: %d pixels on pin %d\n", NEOPIXEL_COUNT, NEOPIXEL_PIN);
Serial.printf("SD Card: %s\n", sdCardAvailable ? "Available" : "Not found");
Serial.printf("Total shows: %d (%d built-in + %d SD)\n",
dynamicShowCount, SHOW_COUNT,
sdCardAvailable ? dynamicShowCount - SHOW_COUNT : 0);
Serial.println();
Serial.println("=== DYNAMIC SHOW CONTROLLER READY ===");
Serial.println("Button Map:");
Serial.println(" 0-9: Scene digits | 16: START | 17: RESET");
Serial.println(" 18-25: Relay toggles | 31: EMERGENCY STOP");
Serial.println("Scene Builder (MUX2 channels 10-14):");
Serial.println(" 26: QUEUE (toggle builder) | 27: UP | 28: DOWN | 29: SELECT | 30: RELOAD SD");
Serial.println();
}
// ==================== MAIN LOOP ====================
void loop() {
// Always read buttons first, even during emergency
readButtons();
// Emergency handling
handleEmergencyMode();
if (sysState.emergencyActive) {
handleEmergencyButtons();
updateDisplay();
if (oledAvailable) updateOLEDDisplay();
delay(100);
return;
}
// Normal button processing
handleButtonPresses();
// Show processing (now uses dynamic shows)
if (sysState.showRunning) updateDynamicShow();
// Display + LEDs
static uint32_t lastDisp = 0;
if (millis() - lastDisp > DISPLAY_UPDATE_MS) {
updateDisplay();
if (oledAvailable) updateOLEDDisplay();
updateAllNeoPixels();
lastDisp = millis();
}
// Safety checks
performSafetyChecks();
delay(10);
}