/**
* SHOWDUINO ESP32-S3 PROFESSIONAL SHOW CONTROLLER v2.6 (+ SD support)
* Complete system with working multiplexer, OLED, 35 scenes, emergency stop, EEPROM
* NeoPixel status + per-button/relay indicators with editable mapping tables
* Using only visible pins from ESP32-S3 WROOM-1
*
* Added:
* - SPI microSD support (non-blocking if missing)
* - /shows, /logs, /config auto-create
* - Boot + event logging helpers
* - External show override: /shows/sceneNNN.shdo (e.g. scene007.shdo)
*/
#include <Arduino.h>
#include <EEPROM.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_NeoPixel.h>
/* ==================== SD CARD (NEW) ==================== */
#include <FS.h>
#include <SD.h>
#include <SPI.h>
#include <ArduinoJson.h>
// --- SD SPI pins: choose CS on a free GPIO. Leave SCK/MOSI/MISO default if your board wires VSPI.
// If your SD module needs explicit pins, uncomment SD_SCK/SD_MISO/SD_MOSI and call SPI.begin() with them.
// NOTE: chosen to avoid your currently-used pins (0–21). Adjust if needed.
// S3 dev boards commonly break out VSPI on: SCK=36, MISO=37, MOSI=35. If yours differ, set them below.
#define SD_CS_PIN 47 // <- pick a free pin for CS (adjust to your board)
#define SD_SPEED_HZ 25000000
//#define SD_SCK 36 // optional: override if your board requires manual SPI pin set
//#define SD_MISO 37
//#define SD_MOSI 35
static bool sdReady = false;
static const char* SD_DIR_SHOWS = "/shows";
static const char* SD_DIR_LOGS = "/logs";
static const char* SD_DIR_CONFIG = "/config";
static void sdMkDir(const char* p) { if (sdReady && !SD.exists(p)) SD.mkdir(p); }
static void sdLog(const char* line) {
if (!sdReady) return;
char fname[48];
// simple rotating name if you reboot a lot; change to RTC timestamp when you have one
snprintf(fname, sizeof(fname), "/logs/boot_%lu.txt", (unsigned long)(millis()/1000));
File f = SD.open(fname, FILE_APPEND);
if (!f) return;
f.println(line);
f.close();
}
static void sdLogf(const char* fmt, ...) {
if (!sdReady) return;
char buf[160];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
sdLog(buf);
}
static bool initSD() {
// If you need explicit pins, uncomment SPI.begin(SD_SCK, SD_MISO, SD_MOSI);
//SPI.begin(SD_SCK, SD_MISO, SD_MOSI);
if (!SD.begin(SD_CS_PIN, SPI, SD_SPEED_HZ)) {
Serial.println("[SD] Card not found or init failed (continuing without SD).");
sdReady = false;
return false;
}
sdReady = true;
sdMkDir(SD_DIR_SHOWS);
sdMkDir(SD_DIR_LOGS);
sdMkDir(SD_DIR_CONFIG);
Serial.println("[SD] Ready. Folders ensured: /shows, /logs, /config");
sdLog("[SD] Boot OK");
return true;
}
// Helpers to parse relay masks flexibly: 0b########, 0x##, or decimal
static uint8_t parseMaskFlexible(JsonVariant v) {
if (v.is<uint32_t>() || v.is<int>()) {
uint32_t m = v.as<uint32_t>();
return (uint8_t)(m & 0xFF);
}
if (v.is<const char*>()) {
String s = v.as<const char*>();
s.trim(); s.toLowerCase();
if (s.startsWith("0b")) {
uint32_t m = 0;
for (int i = 2; i < (int)s.length(); i++) {
m <<= 1;
if (s[i] == '1') m |= 1;
}
return (uint8_t)(m & 0xFF);
}
if (s.startsWith("0x")) {
uint32_t m = (uint32_t) strtoul(s.c_str(), nullptr, 16);
return (uint8_t)(m & 0xFF);
}
// decimal as string
uint32_t m = (uint32_t) strtoul(s.c_str(), nullptr, 10);
return (uint8_t)(m & 0xFF);
}
return 0;
}
/* ==================== 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..20)
// OLED Display (I2C)
#define OLED_SDA 9
#define OLED_SCL 10
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
// ==================== 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; // index for built-in 0..34
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;
// SD show runtime
bool usingSDShow = false; // NEW: true when playing from /shows
} 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
struct ShowStep {
uint16_t duration;
uint8_t relayMask;
const char* action;
};
struct Show {
const char* name;
ShowStep* steps;
uint8_t stepCount;
uint16_t totalDuration;
};
/* ===== SD Show wrapper (NEW) =====
When we load a show from SD, we populate these so the existing
engine can treat it like a "Show".
*/
static ShowStep* sdSteps = nullptr;
static uint8_t sdStepCount = 0;
static char sdShowName[32] = {0};
static Show sdShow = { sdShowName, nullptr, 0, 0 };
static void freeSDShow() {
if (sdSteps) {
free(sdSteps);
sdSteps = nullptr;
}
sdStepCount = 0;
sdShow.steps = nullptr;
sdShow.stepCount = 0;
sdShow.totalDuration = 0;
sdShowName[0] = 0;
}
/* ==================== SHOW LIBRARY (35 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);
// ==================== NeoPixel Role Labels (physical indices) ====================
// Original role labels (keep these consistent with your physical strip)
#define NEO_POWER 0
#define NEO_WORKING 1
#define NEO_UNUSED_2 2
#define NEO_BUTTON_3 3
#define NEO_BUTTON_2 4
#define NEO_BUTTON_1 5
#define NEO_BUTTON_6 6
#define NEO_BUTTON_5 7
#define NEO_BUTTON_4 8
#define NEO_BUTTON_9 9
#define NEO_BUTTON_8 10
#define NEO_BUTTON_7 11
#define NEO_BUTTON_0 12
#define NEO_RELAY_1 13
#define NEO_RELAY_5 14
#define NEO_RELAY_2 15
#define NEO_RELAY_6 16
#define NEO_RELAY_3 17
#define NEO_RELAY_7 18
#define NEO_RELAY_4 19
#define NEO_RELAY_8 20
// ==================== 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 ====================
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);
// ==================== 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}
};
for (int i = 0; i < 4; i++) digitalWrite(controlPin[i], muxChannel[channel][i]);
delayMicroseconds(50);
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");
sdLogf("Manual relay %d: %s", relayIndex + 1, newState ? "ON" : "OFF"); // NEW
}
// ==================== SD SHOW LOADER (NEW) ====================
/*
* Expected JSON (flexible):
* {
* "name": "My Scene 007",
* "steps": [
* {"duration": 3000, "relayMask": "0b10101010"},
* {"duration": 1000, "relayMask": 0}
* ]
* }
* relayMask may be 0-255 (number), "0x##", or "0b########"
*/
static bool loadShowFromSD(uint16_t sceneNum) {
if (!sdReady) return false;
char path[40];
snprintf(path, sizeof(path), "/shows/scene%03u.shdo", (unsigned)sceneNum);
if (!SD.exists(path)) return false;
File f = SD.open(path, FILE_READ);
if (!f) return false;
// Reasonable capacity: up to ~64 steps
StaticJsonDocument<4096> doc;
DeserializationError err = deserializeJson(doc, f);
f.close();
if (err) {
Serial.printf("[SD] JSON parse error: %s\n", err.c_str());
sdLogf("[SD] JSON parse error in %s: %s", path, err.c_str());
return false;
}
JsonArray steps = doc["steps"].as<JsonArray>();
if (!steps || steps.size() == 0) {
Serial.println("[SD] No steps[] in file.");
return false;
}
freeSDShow();
sdStepCount = (uint8_t)min((size_t)255, steps.size());
sdSteps = (ShowStep*) malloc(sizeof(ShowStep) * sdStepCount);
if (!sdSteps) {
Serial.println("[SD] malloc failed for steps");
return false;
}
const char* nm = doc["name"] | "SD Scene";
strlcpy(sdShowName, nm, sizeof(sdShowName));
uint16_t total = 0;
uint8_t i = 0;
for (JsonObject s : steps) {
uint16_t dur = s["duration"] | 0;
uint8_t mask = parseMaskFlexible(s["relayMask"]);
sdSteps[i].duration = dur;
sdSteps[i].relayMask = mask;
sdSteps[i].action = "SD"; // simple label
total += dur;
i++;
if (i >= sdStepCount) break;
}
sdShow.steps = sdSteps;
sdShow.stepCount = sdStepCount;
sdShow.totalDuration = total;
Serial.printf("[SD] Loaded %s (%u steps)\n", sdShowName, sdStepCount);
sdLogf("[SD] Loaded scene%03u.shdo: %s (%u steps)", (unsigned)sceneNum, sdShowName, sdStepCount);
return true;
}
// ==================== SHOW CONTROL ====================
void stopShow(); // forward
void triggerShow(int idx) {
if (idx >= SHOW_COUNT) return;
if (sysState.showRunning) stopShow();
// NEW: external override — if /shows/sceneNNN.shdo exists, use it
uint16_t sceneNum = idx + 1; // human numbering 001..035
sysState.usingSDShow = loadShowFromSD(sceneNum);
sysState.currentShow = idx;
sysState.showRunning = true;
sysState.showStartTime = millis();
sysState.showsTriggered++;
// First step
Show* sh = sysState.usingSDShow ? &sdShow : &showLibrary[idx];
for (int i = 0; i < 8; i++) {
bool on = (sh->steps[0].relayMask >> i) & 1;
setRelay(i, on);
}
Serial.printf("Show started: %s%s\n",
sysState.usingSDShow ? "(SD) " : "", (sysState.usingSDShow ? sdShow.name : showLibrary[idx].name));
sdLogf("Start show %03u: %s%s", (unsigned)sceneNum,
sysState.usingSDShow ? "(SD) " : "",
sysState.usingSDShow ? sdShow.name : showLibrary[idx].name);
}
void executeShowStep(int stepIndex) {
Show* sh = sysState.usingSDShow ? &sdShow : &showLibrary[sysState.currentShow];
ShowStep* step = &sh->steps[stepIndex];
for (int i = 0; i < 8; i++) {
bool on = (step->relayMask >> i) & 1;
setRelay(i, on);
}
Serial.printf("Step %d: %s%s\n", stepIndex, sysState.usingSDShow ? "(SD) " : "", step->action);
}
void updateShow() {
static int currentStep = 0;
static uint32_t stepStart = 0;
if (stepStart == 0) stepStart = millis();
Show* sh = sysState.usingSDShow ? &sdShow : &showLibrary[sysState.currentShow];
if (millis() - stepStart >= sh->steps[currentStep].duration) {
currentStep++;
if (currentStep < sh->stepCount) {
executeShowStep(currentStep);
stepStart = millis();
} else {
stopShow();
currentStep = 0;
stepStart = 0;
updateQueue();
}
}
}
void stopShow() {
for (int i = 0; i < 8; i++) setRelay(i, false);
if (sysState.showRunning) {
uint16_t sn = (sysState.currentShow >= 0) ? (sysState.currentShow + 1) : 0;
sdLogf("Stop show %03u", (unsigned)sn);
}
sysState.showRunning = false;
sysState.currentShow = -1;
if (sysState.usingSDShow) {
freeSDShow(); // NEW: release memory
sysState.usingSDShow = false;
}
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 <= 35) {
if (sysState.builderMode) {
addToQueue(sceneNum - 1);
Serial.println("Scene added! Enter next scene or press SELECT to start queue.");
} else {
triggerShow(sceneNum - 1);
}
sysState.digitIndex = 0;
sysState.sceneReady = false;
memset(sysState.sceneDigits, 0, sizeof(sysState.sceneDigits));
} else {
Serial.printf("Invalid scene: %03d\n", sceneNum);
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;
triggerShow(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) {
sysState.queueIndex++;
if (sysState.queueIndex < sysState.queueCount) {
triggerShow(sysState.sceneQueue[sysState.queueIndex]);
Serial.printf("Queue: Starting scene %d of %d\n", sysState.queueIndex + 1, sysState.queueCount);
} else {
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);
sdLogf("Button %d", i); // NEW: log
showButtonPress(i);
if (i >= 0 && i <= 9) {
handleSceneEntry(i);
}
else if (i == 31) {
Serial.println("=== EMERGENCY STOP ACTIVATED ===");
sysState.emergencyActive = true;
stopShow();
for (int j = 0; j < 8; j++) setRelay(j, false);
}
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();
}
}
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");
}
}
else if (i >= 18 && i <= 25) {
int relayIdx = i - 18;
Serial.printf("Relay toggle button %d (relay %d)\n", i, relayIdx + 1);
if (relayIdx < 8) toggleRelay(relayIdx);
}
else if (i == 26) toggleBuilderMode();
else if (i == 27) {
if (sysState.builderMode) navigateQueue(true);
else Serial.println("UP: Enter builder mode first (press QUEUE)");
}
else if (i == 28) {
if (sysState.builderMode) navigateQueue(false);
else Serial.println("DOWN: Enter builder mode first (press QUEUE)");
}
else if (i == 29) {
if (sysState.builderMode) {
if (sysState.queueCount > 0) {
startQueue();
sysState.builderMode = false;
} else {
Serial.println("Queue is empty! Add scenes first.");
}
} else {
clearQueue();
}
}
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();
}
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 ====================
void updateDisplay() {
if (sysState.emergencyActive) {
Serial.println("[DISPLAY] ⚠️ EMERGENCY STOP ACTIVE - Press START/RESET to clear");
} else if (sysState.showRunning) {
if (sysState.queueMode) {
Serial.printf("[DISPLAY] Queue %d/%d: %s%s\n",
sysState.queueIndex + 1, sysState.queueCount,
sysState.usingSDShow ? "(SD) " : "",
(sysState.usingSDShow ? sdShow.name : showLibrary[sysState.currentShow].name));
} else {
Serial.printf("[DISPLAY] Running: %s%s\n",
sysState.usingSDShow ? "(SD) " : "",
(sysState.usingSDShow ? sdShow.name : showLibrary[sysState.currentShow].name));
}
} else if (sysState.builderMode) {
Serial.printf("[DISPLAY] BUILDER MODE | Queue: %d scenes", sysState.queueCount);
if (sysState.queueCount > 0) {
Serial.printf(" | Cursor: %03d", sysState.sceneQueue[sysState.builderCursor] + 1);
}
Serial.println();
} else if (sysState.sceneReady) {
Serial.printf("[DISPLAY] Scene %03d ready - press START\n", getSceneNumber());
} 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", 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.6"));
display.println(F("Professional"));
display.println(F("Show Controller"));
display.println();
display.println(F("35 Scenes Ready"));
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.6"));
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) {
if (sysState.queueMode) {
display.setCursor(0, 15);
display.printf("QUEUE %d/%d:", sysState.queueIndex + 1, sysState.queueCount);
display.setCursor(0, 25);
display.println(sysState.usingSDShow ? sdShow.name : showLibrary[sysState.currentShow].name);
} else {
display.setCursor(0, 15);
display.println(F("RUNNING:"));
display.setCursor(0, 25);
display.println(sysState.usingSDShow ? sdShow.name : showLibrary[sysState.currentShow].name);
}
display.setCursor(0, 40);
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) {
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 {
display.setCursor(0, 25);
display.printf("Queue: %d scenes", sysState.queueCount);
if (sysState.queueCount > 0) {
display.setCursor(0, 35);
display.printf("Cursor: %03d", sysState.sceneQueue[sysState.builderCursor] + 1);
}
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", sysState.showsTriggered);
if (sysState.queueCount > 0) {
display.setCursor(0, 35);
display.printf("Queue: %d scenes", sysState.queueCount);
} else {
display.setCursor(0, 35);
display.println(F("Enter 3-digit scene"));
}
display.setCursor(0, 50);
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");
sdLog("[SD] Auto-shutdown"); // NEW
}
}
// ==================== NeoPixel IMPLEMENTATION ====================
void initNeoPixels() {
strip.begin();
strip.clear();
strip.setBrightness(64);
setPixelHex(NEO_IDX_POWER, COLOR_GREEN);
setPixelHex(NEO_IDX_WORKING, COLOR_BLUE);
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();
setPixelHex(idx, COLOR_WHITE);
strip.show();
}
void updateAllNeoPixels() {
uint32_t now = millis();
uint16_t period = sysState.showRunning ? 250 : 500;
if (now - lastWorkBlink >= period) {
workBlink = !workBlink;
lastWorkBlink = now;
}
setPixelHex(NEO_IDX_WORKING, workBlink ? COLOR_BLUE : COLOR_OFF);
setPixelHex(NEO_IDX_POWER, sysState.emergencyActive ? COLOR_RED : COLOR_GREEN);
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);
}
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();
}
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, 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.6 (+SD) ===");
EEPROM.begin(EEPROM_SIZE);
loadSettings();
// 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
initOLED();
initNeoPixels();
// SD init (NEW)
initSD();
// (Optional) Pixel walk test
// pixelWalkTest(100);
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: %s\n", sdReady ? "Ready" : "Not present");
Serial.println();
Serial.println("=== 35 SCENE CONTROLLER READY (SD override enabled) ===");
Serial.println("Button Map:");
Serial.println(" 0-9: Scene digits | 16: START | 17: RESET");
Serial.println(" 18-24: Relay toggles | 31: EMERGENCY STOP");
Serial.println("Scene Builder (MUX2 ch 10–13):");
Serial.println(" 26: QUEUE (toggle builder) | 27: UP | 28: DOWN | 29: SELECT");
Serial.println("SD Shows:");
Serial.println(" Place JSON at /shows/sceneNNN.shdo to override built-in Scene NNN.");
Serial.println();
}
// ==================== MAIN LOOP ====================
void loop() {
readButtons();
handleEmergencyMode();
if (sysState.emergencyActive) {
handleEmergencyButtons();
updateDisplay();
if (oledAvailable) updateOLEDDisplay();
delay(100);
return;
}
handleButtonPresses();
if (sysState.showRunning) updateShow();
static uint32_t lastDisp = 0;
if (millis() - lastDisp > DISPLAY_UPDATE_MS) {
updateDisplay();
if (oledAvailable) updateOLEDDisplay();
updateAllNeoPixels();
lastDisp = millis();
}
performSafetyChecks();
delay(10);
}
Loading
esp32-s3-devkitc-1
esp32-s3-devkitc-1
Loading
cd74hc4067
cd74hc4067
Loading
cd74hc4067
cd74hc4067
Loading
ssd1306
ssd1306