/**
* SHOWDUINO ESP32-S3 PROFESSIONAL SHOW CONTROLLER v2.9 COMPLETE
* =============================================================
*
* A comprehensive show control system with multi-line FX engine, settings menu,
* and advanced emergency management. Built from the ChatGPT conversation roadmap.
*
* FEATURES:
* - 8 relay control via multiplexed buttons (32 buttons total)
* - 4 dedicated FX NeoPixel lines with per-pixel segment control
* - 6+ advanced effects: solid, flash, chase, twinkle, gradient, wave
* - SD-based show and FX definition system
* - Interactive OLED settings menu with EEPROM + SD persistence
* - Scene builder and queue system
* - Configurable emergency behavior with FX override
* - Professional layered FX system with priority blending
*
* Author: TJ/Tobe - Based on ChatGPT FX Engine Conversation
* Version: 2.9.0
* Date: 2024
*/
#include <Arduino.h>
#include <EEPROM.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <FS.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_NeoPixel.h>
// ==================== HARDWARE CONFIGURATION ====================
// ESP32-S3 Pin Assignments
// Multiplexer Control (74HC4067 x2 for 32 buttons)
#define MUX_S0 12 // GPIO12 - Binary select 0
#define MUX_S1 14 // GPIO14 - Binary select 1
#define MUX_S2 13 // GPIO13 - Binary select 2
#define MUX_S3 21 // GPIO21 - Binary select 3
#define MUX1_SIGNAL 4 // GPIO4 - MUX1 analog input
#define MUX2_SIGNAL 5 // GPIO5 - MUX2 analog input
// Relay Outputs (8 channels)
const int RELAY_PINS[8] = {6, 7, 15, 16, 17, 18, 8, 3};
// NeoPixel Configuration
#define INDICATOR_PIN 1 // Indicator strip (buttons/relays/status)
#define INDICATOR_COUNT 30 // Number of indicator pixels
// FX NeoPixel Lines (4 dedicated strips)
#define NUM_FX_LINES 4
const int FX_PINS[NUM_FX_LINES] = {38, 39, 40, 41};
const int FX_LENGTHS[NUM_FX_LINES] = {30, 30, 24, 16};
// OLED Display (I2C)
#define OLED_SDA 9 // I2C Data
#define OLED_SCL 10 // I2C Clock
#define OLED_WIDTH 128
#define OLED_HEIGHT 64
#define OLED_ADDRESS 0x3C
// SD Card (SPI)
#define SD_CS 47 // Chip Select
#define SD_MOSI 35 // Master Out Slave In
#define SD_MISO 37 // Master In Slave Out
#define SD_SCK 36 // Serial Clock
// System
#define STATUS_LED 2 // Built-in LED
// ==================== SYSTEM CONSTANTS ====================
#define EEPROM_SIZE 512
#define EEPROM_SETTINGS_ADDR 0
#define BUTTON_DEBOUNCE_MS 200
#define BUTTON_THRESHOLD 2000
#define LONG_PRESS_MS 800
#define DISPLAY_UPDATE_MS 250
#define AUTO_SHUTOFF_MS 600000
#define MAX_SHOWS 85
#define MAX_STEPS_PER_SHOW 20
#define MAX_FX_SEGMENTS 32
#define SCENE_TIMEOUT_MS 5000
// ==================== DATA STRUCTURES ====================
// Enhanced Show Step with FX support
struct ShowStep {
uint16_t duration; // Duration in milliseconds
uint8_t relayMask; // 8-bit relay pattern (MSB = Relay 1)
char fxFile[32]; // Optional FX file path
};
// Show Definition
struct Show {
char name[32]; // Show name
uint8_t stepCount; // Number of steps
uint16_t totalDuration; // Total show duration
ShowStep steps[MAX_STEPS_PER_SHOW];
bool fromSD; // Loaded from SD card
};
// FX Segment with advanced features
struct FXSegment {
uint8_t line; // FX line index (0-3)
uint8_t start; // Start pixel
uint8_t length; // Segment length
uint8_t effectID; // Effect type
uint32_t color; // Primary color
uint16_t speed; // Effect speed (ms)
uint8_t brightness; // Brightness (0-255)
uint16_t fade; // Fade duration (0=off)
uint8_t amplitude; // Wave amplitude
uint32_t colors[4]; // Gradient colors
uint8_t colorCount; // Number of gradient colors
uint8_t priority; // Layer priority
};
// System Settings (EEPROM persistent)
struct Settings {
uint16_t magic; // Validity check
bool autoShutoff; // Auto shutdown after timeout
uint8_t maxShowDuration; // Max duration in minutes
bool enableSafetyLimits; // Enable safety checks
bool killRelaysOnEmergency; // Kill relays during emergency
uint8_t emergencyRelayIndex; // Relay to preserve (255=none)
uint8_t indicatorBrightness; // Indicator strip brightness
uint8_t fxBrightness; // FX strip brightness
};
// System State
struct SystemState {
// Relays
bool relayStates[8];
uint32_t relayOnTime[8];
// Emergency
bool emergencyActive;
bool emergencyFlash;
uint32_t emergencyStart;
// Show Control
bool showRunning;
int16_t currentShow;
uint16_t currentStep;
uint32_t showStartTime;
uint32_t stepStartTime;
uint32_t lastActivity;
uint16_t showsTriggered;
// Buttons
bool buttonStates[32];
bool lastButtonStates[32];
uint32_t lastButtonPress[32];
uint32_t longPressStart[32];
// Scene Selection
char sceneDigits[4];
uint8_t digitIndex;
uint32_t lastDigitTime;
bool sceneReady;
// Queue System
int16_t sceneQueue[10];
uint8_t queueCount;
uint8_t queueIndex;
bool queueMode;
bool builderMode;
int16_t builderCursor;
uint32_t lastBuilderActivity;
};
// Settings Menu State
struct MenuState {
enum State { HIDDEN, SETTINGS, EDIT_VALUE } state;
int16_t cursor;
bool editing;
uint32_t toastUntil;
String toastMsg;
};
// ==================== GLOBAL INSTANCES ====================
Settings settings = {
.magic = 0xABCD,
.autoShutoff = true,
.maxShowDuration = 5,
.enableSafetyLimits = true,
.killRelaysOnEmergency = true,
.emergencyRelayIndex = 255,
.indicatorBrightness = 64,
.fxBrightness = 128
};
SystemState sysState = {};
MenuState menuState = {};
// Hardware instances
Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, -1);
Adafruit_NeoPixel indicator(INDICATOR_COUNT, INDICATOR_PIN, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel fxStrips[NUM_FX_LINES] = {
Adafruit_NeoPixel(FX_LENGTHS[0], FX_PINS[0], NEO_GRB + NEO_KHZ800),
Adafruit_NeoPixel(FX_LENGTHS[1], FX_PINS[1], NEO_GRB + NEO_KHZ800),
Adafruit_NeoPixel(FX_LENGTHS[2], FX_PINS[2], NEO_GRB + NEO_KHZ800),
Adafruit_NeoPixel(FX_LENGTHS[3], FX_PINS[3], NEO_GRB + NEO_KHZ800)
};
// Data arrays
Show showLibrary[MAX_SHOWS];
uint8_t totalShowCount = 0;
FXSegment activeFXSegments[MAX_FX_SEGMENTS];
uint8_t activeFXCount = 0;
// System flags
bool sdAvailable = false;
bool oledAvailable = false;
// ==================== COLOR DEFINITIONS ====================
#define COLOR_OFF 0x000000
#define COLOR_RED 0xFF0000
#define COLOR_GREEN 0x00FF00
#define COLOR_BLUE 0x0000FF
#define COLOR_WHITE 0xFFFFFF
#define COLOR_YELLOW 0xFFFF00
#define COLOR_CYAN 0x00FFFF
#define COLOR_MAGENTA 0xFF00FF
#define COLOR_ORANGE 0xFF8000
#define COLOR_PURPLE 0x800080
// Indicator pixel mapping
#define IND_POWER 0
#define IND_WORKING 1
#define IND_SD_STATUS 2
#define IND_EMERGENCY 3
const int IND_BUTTONS[10] = {11, 4, 3, 2, 7, 6, 5, 10, 9, 8};
const int IND_RELAYS[8] = {14, 15, 16, 17, 18, 19, 20, 21};
// Button flash timing
uint32_t buttonFlashTime[10] = {0};
bool workingBlink = false;
uint32_t lastWorkingBlink = 0;
// ==================== UTILITY FUNCTIONS ====================
// Convert 8-character binary string to byte
uint8_t parseBinary8(const char* str) {
uint8_t result = 0;
for (int i = 0; i < 8; i++) {
if (str[i] == '1') {
result |= (1 << (7 - i)); // MSB first
}
}
return result;
}
// Color blending for layered FX
uint32_t blendColors(uint32_t base, uint32_t overlay) {
uint8_t r1 = (base >> 16) & 0xFF;
uint8_t g1 = (base >> 8) & 0xFF;
uint8_t b1 = base & 0xFF;
uint8_t r2 = (overlay >> 16) & 0xFF;
uint8_t g2 = (overlay >> 8) & 0xFF;
uint8_t b2 = overlay & 0xFF;
// Additive blending with saturation
uint8_t r = min(255, r1 + r2);
uint8_t g = min(255, g1 + g2);
uint8_t b = min(255, b1 + b2);
return (r << 16) | (g << 8) | b;
}
// Apply fade modulation
uint8_t applyFade(uint8_t value, const FXSegment& seg) {
if (seg.fade == 0) return value;
float phase = (float)(millis() % seg.fade) / seg.fade;
float factor = (phase < 0.5f) ? (phase * 2.0f) : (2.0f - phase * 2.0f);
return (uint8_t)(value * factor);
}
// Adjust color with brightness and fade
uint32_t adjustColor(uint32_t color, const FXSegment& seg) {
uint8_t r = (color >> 16) & 0xFF;
uint8_t g = (color >> 8) & 0xFF;
uint8_t b = color & 0xFF;
// Apply brightness
r = (r * seg.brightness) / 255;
g = (g * seg.brightness) / 255;
b = (b * seg.brightness) / 255;
// Apply fade
r = applyFade(r, seg);
g = applyFade(g, seg);
b = applyFade(b, seg);
return fxStrips[seg.line].Color(r, g, b);
}
// ==================== HARDWARE INITIALIZATION ====================
void initPins() {
// Multiplexer control pins
pinMode(MUX_S0, OUTPUT);
pinMode(MUX_S1, OUTPUT);
pinMode(MUX_S2, OUTPUT);
pinMode(MUX_S3, OUTPUT);
// Relay pins
for (int i = 0; i < 8; i++) {
pinMode(RELAY_PINS[i], OUTPUT);
digitalWrite(RELAY_PINS[i], LOW);
}
// Status LED
pinMode(STATUS_LED, OUTPUT);
digitalWrite(STATUS_LED, LOW);
Serial.println("Hardware pins initialized");
}
bool initOLED() {
Wire.begin(OLED_SDA, OLED_SCL);
if (display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println(F("SHOWDUINO v2.9"));
display.println(F("Initializing..."));
display.display();
Serial.println("OLED display initialized");
return true;
}
Serial.println("OLED initialization failed");
return false;
}
void initIndicatorPixels() {
indicator.begin();
indicator.setBrightness(settings.indicatorBrightness);
indicator.clear();
// Set initial states
indicator.setPixelColor(IND_POWER, COLOR_GREEN);
indicator.setPixelColor(IND_WORKING, COLOR_BLUE);
// Clear button indicators
for (int i = 0; i < 10; i++) {
buttonFlashTime[i] = 0;
indicator.setPixelColor(IND_BUTTONS[i], COLOR_OFF);
}
// Clear relay indicators
for (int i = 0; i < 8; i++) {
indicator.setPixelColor(IND_RELAYS[i], COLOR_OFF);
}
indicator.show();
Serial.println("Indicator pixels initialized");
}
void initFXPixels() {
for (int i = 0; i < NUM_FX_LINES; i++) {
fxStrips[i].begin();
fxStrips[i].setBrightness(settings.fxBrightness);
fxStrips[i].clear();
fxStrips[i].show();
}
Serial.printf("FX pixel lines initialized: %d lines\n", NUM_FX_LINES);
}
bool initSD() {
SPI.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);
if (SD.begin(SD_CS)) {
uint8_t cardType = SD.cardType();
if (cardType != CARD_NONE) {
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf("SD card initialized: %lluMB\n", cardSize);
return true;
}
}
Serial.println("SD card not available");
return false;
}
void initEEPROM() {
EEPROM.begin(EEPROM_SIZE);
Settings temp;
EEPROM.get(EEPROM_SETTINGS_ADDR, temp);
if (temp.magic == 0xABCD) {
settings = temp;
Serial.println("Settings loaded from EEPROM");
} else {
// First boot - save defaults
EEPROM.put(EEPROM_SETTINGS_ADDR, settings);
EEPROM.commit();
Serial.println("Default settings saved to EEPROM");
}
}
// ==================== BUTTON INPUT SYSTEM ====================
int readMux(int muxNumber, int channel) {
// Set mux channel
digitalWrite(MUX_S0, (channel & 0x01) ? HIGH : LOW);
digitalWrite(MUX_S1, (channel & 0x02) ? HIGH : LOW);
digitalWrite(MUX_S2, (channel & 0x04) ? HIGH : LOW);
digitalWrite(MUX_S3, (channel & 0x08) ? HIGH : LOW);
delayMicroseconds(50); // Settling time
int pin = (muxNumber == 1) ? MUX1_SIGNAL : MUX2_SIGNAL;
return analogRead(pin);
}
bool isButtonPressed(int buttonNum) {
int mux = (buttonNum < 16) ? 1 : 2;
int channel = buttonNum % 16;
return (readMux(mux, channel) > BUTTON_THRESHOLD);
}
void readAllButtons() {
for (int i = 0; i < 32; i++) {
sysState.lastButtonStates[i] = sysState.buttonStates[i];
sysState.buttonStates[i] = isButtonPressed(i);
}
}
void flashButtonIndicator(int buttonNum) {
if (buttonNum >= 0 && buttonNum <= 9) {
buttonFlashTime[buttonNum] = millis();
indicator.setPixelColor(IND_BUTTONS[buttonNum], COLOR_WHITE);
indicator.show();
}
}
// ==================== RELAY CONTROL ====================
void setRelay(int index, bool state) {
if (index >= 0 && index < 8) {
digitalWrite(RELAY_PINS[index], state ? HIGH : LOW);
sysState.relayStates[index] = state;
if (state) {
sysState.relayOnTime[index] = millis();
}
// Update indicator
indicator.setPixelColor(IND_RELAYS[index], state ? COLOR_ORANGE : COLOR_OFF);
}
}
void toggleRelay(int index) {
setRelay(index, !sysState.relayStates[index]);
Serial.printf("Manual relay %d: %s\n", index + 1, sysState.relayStates[index] ? "ON" : "OFF");
}
void setAllRelays(uint8_t mask) {
for (int i = 0; i < 8; i++) {
bool state = (mask >> (7 - i)) & 1; // MSB first
setRelay(i, state);
}
}
void clearAllRelays() {
setAllRelays(0x00);
}
// ==================== FX SYSTEM ====================
void applyEffect(const FXSegment& seg) {
Adafruit_NeoPixel& strip = fxStrips[seg.line];
uint32_t now = millis();
switch (seg.effectID) {
case 0: // Solid
for (int p = seg.start; p < seg.start + seg.length; p++) {
strip.setPixelColor(p, adjustColor(seg.color, seg));
}
break;
case 1: // Flash
if ((now / seg.speed) % 2 == 0) {
for (int p = seg.start; p < seg.start + seg.length; p++) {
strip.setPixelColor(p, adjustColor(seg.color, seg));
}
}
break;
case 2: // Chase
{
int pos = (now / seg.speed) % seg.length;
for (int p = 0; p < seg.length; p++) {
uint32_t color = (p == pos) ? adjustColor(seg.color, seg) : 0;
strip.setPixelColor(seg.start + p, color);
}
}
break;
case 3: // Twinkle
for (int p = seg.start; p < seg.start + seg.length; p++) {
if (random(1000) < 8) { // Sparse twinkles
strip.setPixelColor(p, adjustColor(seg.color, seg));
}
}
break;
case 4: // Gradient
if (seg.colorCount >= 2) {
for (int i = 0; i < seg.length; i++) {
float pos = (float)i / (seg.length - 1);
int stop = (int)(pos * (seg.colorCount - 1));
float localPos = (pos * (seg.colorCount - 1)) - stop;
if (stop < seg.colorCount - 1) {
uint8_t r1 = (seg.colors[stop] >> 16) & 0xFF;
uint8_t g1 = (seg.colors[stop] >> 8) & 0xFF;
uint8_t b1 = seg.colors[stop] & 0xFF;
uint8_t r2 = (seg.colors[stop + 1] >> 16) & 0xFF;
uint8_t g2 = (seg.colors[stop + 1] >> 8) & 0xFF;
uint8_t b2 = seg.colors[stop + 1] & 0xFF;
uint8_t r = r1 + (r2 - r1) * localPos;
uint8_t g = g1 + (g2 - g1) * localPos;
uint8_t b = b1 + (b2 - b1) * localPos;
uint32_t gradColor = strip.Color(r, g, b);
strip.setPixelColor(seg.start + i, adjustColor(gradColor, seg));
}
}
}
break;
case 5: // Wave
for (int i = 0; i < seg.length; i++) {
float phase = (float)(now / seg.speed) + (i * (seg.amplitude > 0 ? seg.amplitude : 1));
float intensity = (sin(phase * 0.1f) + 1.0f) * 0.5f;
uint8_t r = ((seg.color >> 16) & 0xFF) * intensity;
uint8_t g = ((seg.color >> 8) & 0xFF) * intensity;
uint8_t b = (seg.color & 0xFF) * intensity;
uint32_t waveColor = strip.Color(r, g, b);
strip.setPixelColor(seg.start + i, adjustColor(waveColor, seg));
}
break;
}
}
void applyEmergencyFX() {
if (!sysState.emergencyActive) return;
uint32_t now = millis();
if (sysState.emergencyFlash && (now - sysState.emergencyStart < 2000)) {
// Stage 1: Red flashing (first 2 seconds)
bool on = ((now / 250) % 2 == 0);
for (int line = 0; line < NUM_FX_LINES; line++) {
for (int p = 0; p < FX_LENGTHS[line]; p++) {
fxStrips[line].setPixelColor(p, on ? COLOR_RED : COLOR_OFF);
}
}
} else {
// Stage 2: White emergency lighting
sysState.emergencyFlash = false;
for (int line = 0; line < NUM_FX_LINES; line++) {
for (int p = 0; p < FX_LENGTHS[line]; p++) {
fxStrips[line].setPixelColor(p, COLOR_WHITE);
}
}
}
}
void updateFXSystem() {
// Clear all FX strips
for (int i = 0; i < NUM_FX_LINES; i++) {
fxStrips[i].clear();
}
// Apply normal FX segments
for (int i = 0; i < activeFXCount; i++) {
applyEffect(activeFXSegments[i]);
}
// Apply emergency override if active
applyEmergencyFX();
// Update all strips
for (int i = 0; i < NUM_FX_LINES; i++) {
fxStrips[i].show();
}
}
void clearFXSegments() {
activeFXCount = 0;
for (int i = 0; i < NUM_FX_LINES; i++) {
fxStrips[i].clear();
fxStrips[i].show();
}
}
// ==================== SD FILE SYSTEM ====================
void createDirectoryStructure() {
if (!sdAvailable) return;
// Create directories
if (!SD.exists("/shows")) SD.mkdir("/shows");
if (!SD.exists("/fx")) SD.mkdir("/fx");
if (!SD.exists("/config")) SD.mkdir("/config");
Serial.println("SD directory structure created");
}
void createExampleFiles() {
if (!sdAvailable) return;
// Example show with FX
File file = SD.open("/shows/fx_demo.txt", FILE_WRITE);
if (file) {
file.println("# Name: FX Demo Show");
file.println("# Enhanced format: duration_ms,relay_binary,fx_file");
file.println("2000,10000000,red_solid.txt");
file.println("2000,01000000,blue_chase.txt");
file.println("3000,11000000,rainbow_fade.txt");
file.println("1000,00000000");
file.close();
}
// Example FX files
file = SD.open("/fx/red_solid.txt", FILE_WRITE);
if (file) {
file.println("# Red solid effect");
file.println("FX: line0 0-29 solid 255,0,0");
file.close();
}
file = SD.open("/fx/blue_chase.txt", FILE_WRITE);
if (file) {
file.println("# Blue chase effect");
file.println("FX: line1 0-23 chase 0,0,255 200");
file.close();
}
file = SD.open("/fx/rainbow_fade.txt", FILE_WRITE);
if (file) {
file.println("# Rainbow gradient with fade");
file.println("FX: line0 0-29 gradient 255,0,0|0,255,0|0,0,255 fade=2000");
file.close();
}
// Settings file
file = SD.open("/config/settings.txt", FILE_WRITE);
if (file) {
file.println("# Showduino Settings");
file.println("autoShutoff=1");
file.println("killRelaysOnEmergency=1");
file.println("emergencyRelayIndex=255");
file.println("maxShowDuration=5");
file.close();
}
Serial.println("Example files created");
}
bool parseFXFile(const char* filename) {
if (!sdAvailable) return false;
String path = "/fx/" + String(filename);
File file = SD.open(path);
if (!file) {
Serial.printf("FX file not found: %s\n", path.c_str());
return false;
}
activeFXCount = 0;
while (file.available() && activeFXCount < MAX_FX_SEGMENTS) {
String line = file.readStringUntil('\n');
line.trim();
if (!line.startsWith("FX:")) continue;
// Parse: FX: line0 0-15 solid 255,0,0 500
FXSegment seg = {};
seg.brightness = 255;
seg.speed = 200;
seg.priority = 1;
int lineIdx, start, end, r = 255, g = 0, b = 0, speed = 200;
char effect[16];
int parsed = sscanf(line.c_str(), "FX: line%d %d-%d %15s %d,%d,%d %d",
&lineIdx, &start, &end, effect, &r, &g, &b, &speed);
if (parsed >= 4 && lineIdx < NUM_FX_LINES) {
seg.line = lineIdx;
seg.start = start;
seg.length = end - start + 1;
seg.color = fxStrips[lineIdx].Color(r, g, b);
seg.speed = speed;
// Map effect names
if (strcmp(effect, "solid") == 0) seg.effectID = 0;
else if (strcmp(effect, "flash") == 0) seg.effectID = 1;
else if (strcmp(effect, "chase") == 0) seg.effectID = 2;
else if (strcmp(effect, "twinkle") == 0) seg.effectID = 3;
else if (strcmp(effect, "gradient") == 0) seg.effectID = 4;
else if (strcmp(effect, "wave") == 0) seg.effectID = 5;
activeFXSegments[activeFXCount++] = seg;
}
}
file.close();
Serial.printf("Loaded %d FX segments from %s\n", activeFXCount, filename);
return true;
}
bool loadShow(const char* filename) {
if (totalShowCount >= MAX_SHOWS) return false;
if (!sdAvailable) return false;
String path = "/shows/" + String(filename);
File file = SD.open(path);
if (!file) return false;
Show& show = showLibrary[totalShowCount];
show.fromSD = true;
show.stepCount = 0;
show.totalDuration = 0;
// Default name from filename
String name = String(filename);
name.replace(".txt", "");
strncpy(show.name, name.c_str(), 31);
show.name[31] = 0;
while (file.available() && show.stepCount < MAX_STEPS_PER_SHOW) {
String line = file.readStringUntil('\n');
line.trim();
if (line.startsWith("# Name:")) {
String showName = line.substring(7);
showName.trim();
strncpy(show.name, showName.c_str(), 31);
show.name[31] = 0;
continue;
}
if (line.startsWith("#") || line.length() < 10) continue;
int firstComma = line.indexOf(',');
int secondComma = line.indexOf(',', firstComma + 1);
if (firstComma > 0) {
ShowStep& step = show.steps[show.stepCount];
step.duration = line.substring(0, firstComma).toInt();
String maskStr = line.substring(firstComma + 1,
secondComma > 0 ? secondComma : line.length());
maskStr.trim();
step.relayMask = parseBinary8(maskStr.c_str());
// Parse optional FX file
if (secondComma > 0) {
String fxFile = line.substring(secondComma + 1);
fxFile.trim();
strncpy(step.fxFile, fxFile.c_str(), 31);
step.fxFile[31] = 0;
} else {
step.fxFile[0] = 0;
}
show.totalDuration += step.duration;
show.stepCount++;
}
}
file.close();
if (show.stepCount > 0) {
totalShowCount++;
Serial.printf("Loaded show: %s (%d steps)\n", show.name, show.stepCount);
return true;
}
return false;
}
void loadAllShows() {
if (!sdAvailable) return;
File root = SD.open("/shows");
if (!root) return;
File file = root.openNextFile();
while (file && totalShowCount < MAX_SHOWS) {
if (!file.isDirectory()) {
String filename = file.name();
if (filename.endsWith(".txt") && !filename.startsWith("README")) {
loadShow(filename.c_str());
}
}
file = root.openNextFile();
}
root.close();
Serial.printf("Total shows loaded: %d\n", totalShowCount);
}
// ==================== SETTINGS MANAGEMENT ====================
void saveSettingsToEEPROM() {
EEPROM.put(EEPROM_SETTINGS_ADDR, settings);
EEPROM.commit();
}
bool saveSettingsToSD() {
if (!sdAvailable) return false;
File file = SD.open("/config/settings.txt", FILE_WRITE);
if (!file) return false;
file.println("# Showduino Settings");
file.printf("autoShutoff=%d\n", settings.autoShutoff ? 1 : 0);
file.printf("maxShowDuration=%d\n", settings.maxShowDuration);
file.printf("enableSafetyLimits=%d\n", settings.enableSafetyLimits ? 1 : 0);
file.printf("killRelaysOnEmergency=%d\n", settings.killRelaysOnEmergency ? 1 : 0);
file.printf("emergencyRelayIndex=%d\n", settings.emergencyRelayIndex);
file.printf("indicatorBrightness=%d\n", settings.indicatorBrightness);
file.printf("fxBrightness=%d\n", settings.fxBrightness);
file.close();
return true;
}
bool loadSettingsFromSD() {
if (!sdAvailable) return false;
File file = SD.open("/config/settings.txt");
if (!file) return false;
Settings temp = settings;
while (file.available()) {
String line = file.readStringUntil('\n');
line.trim();
if (line.startsWith("#") || line.length() == 0) continue;
int eq = line.indexOf('=');
if (eq < 0) continue;
String key = line.substring(0, eq);
String value = line.substring(eq + 1);
key.trim();
value.trim();
if (key == "autoShutoff") temp.autoShutoff = (value.toInt() == 1);
else if (key == "maxShowDuration") temp.maxShowDuration = value.toInt();
else if (key == "enableSafetyLimits") temp.enableSafetyLimits = (value.toInt() == 1);
else if (key == "killRelaysOnEmergency") temp.killRelaysOnEmergency = (value.toInt() == 1);
else if (key == "emergencyRelayIndex") temp.emergencyRelayIndex = value.toInt();
else if (key == "indicatorBrightness") temp.indicatorBrightness = value.toInt();
else if (key == "fxBrightness") temp.fxBrightness = value.toInt();
}
file.close();
settings = temp;
saveSettingsToEEPROM();
return true;
}
// ==================== SETTINGS MENU SYSTEM ====================
const char* menuItems[] = {
"Auto Shutoff",
"Safety Limits",
"Kill Relays on EMG",
"EMG Relay Index",
"Max Show Duration",
"Indicator Brightness",
"FX Brightness",
"Load Settings from SD",
"Save Settings to SD",
"Back"
};
const int menuItemCount = 10;
void showToast(const String& message, uint16_t duration = 1200) {
menuState.toastMsg = message;
menuState.toastUntil = millis() + duration;
}
void showSettingsMenu() {
menuState.state = MenuState::SETTINGS;
menuState.cursor = 0;
menuState.editing = false;
showToast("Settings Menu");
}
void hideSettingsMenu() {
menuState.state = MenuState::HIDDEN;
menuState.editing = false;
}
void adjustEmergencyRelayIndex(bool increase) {
if (increase) {
if (settings.emergencyRelayIndex == 255) settings.emergencyRelayIndex = 0;
else if (settings.emergencyRelayIndex < 7) settings.emergencyRelayIndex++;
else settings.emergencyRelayIndex = 255;
} else {
if (settings.emergencyRelayIndex == 255) settings.emergencyRelayIndex = 7;
else if (settings.emergencyRelayIndex > 0) settings.emergencyRelayIndex--;
else settings.emergencyRelayIndex = 255;
}
}
void handleSettingsMenuButton(int buttonNum) {
if (menuState.state == MenuState::HIDDEN) return;
if (!menuState.editing) {
if (buttonNum == 27) { // UP
menuState.cursor = (menuState.cursor - 1 + menuItemCount) % menuItemCount;
}
else if (buttonNum == 28) { // DOWN
menuState.cursor = (menuState.cursor + 1) % menuItemCount;
}
else if (buttonNum == 16) { // START
switch (menuState.cursor) {
case 0: // Auto Shutoff
settings.autoShutoff = !settings.autoShutoff;
saveSettingsToEEPROM();
showToast(String("Auto Shutoff: ") + (settings.autoShutoff ? "ON" : "OFF"));
break;
case 1: // Safety Limits
settings.enableSafetyLimits = !settings.enableSafetyLimits;
saveSettingsToEEPROM();
showToast(String("Safety Limits: ") + (settings.enableSafetyLimits ? "ON" : "OFF"));
break;
case 2: // Kill Relays on EMG
settings.killRelaysOnEmergency = !settings.killRelaysOnEmergency;
saveSettingsToEEPROM();
showToast(String("Kill Relays: ") + (settings.killRelaysOnEmergency ? "ON" : "OFF"));
break;
case 3: // EMG Relay Index
case 4: // Max Show Duration
case 5: // Indicator Brightness
case 6: // FX Brightness
menuState.editing = true;
showToast("Editing...");
break;
case 7: // Load from SD
if (loadSettingsFromSD()) {
showToast("Loaded from SD");
// Update brightness
indicator.setBrightness(settings.indicatorBrightness);
for (int i = 0; i < NUM_FX_LINES; i++) {
fxStrips[i].setBrightness(settings.fxBrightness);
}
} else {
showToast("Load FAILED");
}
break;
case 8: // Save to SD
showToast(saveSettingsToSD() ? "Saved to SD" : "Save FAILED");
break;
case 9: // Back
hideSettingsMenu();
break;
}
}
else if (buttonNum == 17) { // RESET - exit menu
hideSettingsMenu();
}
}
else { // Editing mode
if (buttonNum == 27) { // UP
switch (menuState.cursor) {
case 3: adjustEmergencyRelayIndex(true); break;
case 4: if (settings.maxShowDuration < 180) settings.maxShowDuration++; break;
case 5: if (settings.indicatorBrightness < 255) settings.indicatorBrightness += 16; break;
case 6: if (settings.fxBrightness < 255) settings.fxBrightness += 16; break;
}
}
else if (buttonNum == 28) { // DOWN
switch (menuState.cursor) {
case 3: adjustEmergencyRelayIndex(false); break;
case 4: if (settings.maxShowDuration > 1) settings.maxShowDuration--; break;
case 5: if (settings.indicatorBrightness > 16) settings.indicatorBrightness -= 16; break;
case 6: if (settings.fxBrightness > 16) settings.fxBrightness -= 16; break;
}
}
else if (buttonNum == 16) { // START - save
saveSettingsToEEPROM();
menuState.editing = false;
// Apply brightness changes immediately
if (menuState.cursor == 5) {
indicator.setBrightness(settings.indicatorBrightness);
} else if (menuState.cursor == 6) {
for (int i = 0; i < NUM_FX_LINES; i++) {
fxStrips[i].setBrightness(settings.fxBrightness);
}
}
showToast("Saved");
}
else if (buttonNum == 17) { // RESET - cancel
menuState.editing = false;
showToast("Cancelled");
}
}
}
void drawSettingsMenu() {
if (menuState.state == MenuState::HIDDEN) return;
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println(F("SETTINGS MENU"));
display.drawLine(0, 9, 127, 9, SSD1306_WHITE);
// Show 6 items at a time to fit screen
int startItem = max(0, menuState.cursor - 5);
int endItem = min(menuItemCount - 1, startItem + 5);
for (int i = startItem; i <= endItem; i++) {
int y = 12 + (i - startItem) * 9;
display.setCursor(0, y);
if (i == menuState.cursor) display.print("> ");
else display.print(" ");
display.print(menuItems[i]);
display.print(": ");
// Show values
switch (i) {
case 0: display.print(settings.autoShutoff ? "ON" : "OFF"); break;
case 1: display.print(settings.enableSafetyLimits ? "ON" : "OFF"); break;
case 2: display.print(settings.killRelaysOnEmergency ? "ON" : "OFF"); break;
case 3:
if (menuState.editing && i == menuState.cursor) display.print("[");
if (settings.emergencyRelayIndex == 255) display.print("None");
else display.print(settings.emergencyRelayIndex + 1);
if (menuState.editing && i == menuState.cursor) display.print("]");
break;
case 4:
if (menuState.editing && i == menuState.cursor) display.print("[");
display.print(settings.maxShowDuration);
display.print("m");
if (menuState.editing && i == menuState.cursor) display.print("]");
break;
case 5:
if (menuState.editing && i == menuState.cursor) display.print("[");
display.print(settings.indicatorBrightness);
if (menuState.editing && i == menuState.cursor) display.print("]");
break;
case 6:
if (menuState.editing && i == menuState.cursor) display.print("[");
display.print(settings.fxBrightness);
if (menuState.editing && i == menuState.cursor) display.print("]");
break;
}
}
// Toast message
if (menuState.toastUntil && (int32_t)(millis() - menuState.toastUntil) < 0) {
display.setCursor(0, 55);
display.print(menuState.toastMsg);
}
display.display();
}
// ==================== SHOW CONTROL SYSTEM ====================
void initBuiltInShows() {
totalShowCount = 0;
// Create basic test shows
for (int i = 1; i <= 8; i++) {
Show& show = showLibrary[totalShowCount];
sprintf(show.name, "Relay %d Test", i);
show.stepCount = 2;
show.totalDuration = 4000;
show.fromSD = false;
show.steps[0].duration = 3000;
show.steps[0].relayMask = (1 << (8 - i)); // MSB first
show.steps[0].fxFile[0] = 0;
show.steps[1].duration = 1000;
show.steps[1].relayMask = 0x00;
show.steps[1].fxFile[0] = 0;
totalShowCount++;
}
// All relays test
Show& show = showLibrary[totalShowCount];
strcpy(show.name, "All Relays Test");
show.stepCount = 2;
show.totalDuration = 5000;
show.fromSD = false;
show.steps[0].duration = 4000;
show.steps[0].relayMask = 0xFF;
show.steps[0].fxFile[0] = 0;
show.steps[1].duration = 1000;
show.steps[1].relayMask = 0x00;
show.steps[1].fxFile[0] = 0;
totalShowCount++;
Serial.printf("Built-in shows loaded: %d\n", totalShowCount);
}
void executeStep(int stepIndex) {
if (sysState.currentShow < 0 || stepIndex < 0) return;
Show& show = showLibrary[sysState.currentShow];
if (stepIndex >= show.stepCount) return;
ShowStep& step = show.steps[stepIndex];
// Apply relay changes
setAllRelays(step.relayMask);
// Load FX if specified
if (strlen(step.fxFile) > 0) {
parseFXFile(step.fxFile);
} else {
clearFXSegments();
}
Serial.printf("Step %d: Relays=%02X, FX=%s\n",
stepIndex, step.relayMask, step.fxFile[0] ? step.fxFile : "none");
}
void startShow(int showIndex) {
if (showIndex < 0 || showIndex >= totalShowCount) return;
stopShow();
sysState.currentShow = showIndex;
sysState.currentStep = 0;
sysState.showRunning = true;
sysState.showStartTime = millis();
sysState.stepStartTime = millis();
sysState.showsTriggered++;
executeStep(0);
Serial.printf("Started show: %s\n", showLibrary[showIndex].name);
}
void updateShowProgress() {
if (!sysState.showRunning) return;
Show& show = showLibrary[sysState.currentShow];
ShowStep& step = show.steps[sysState.currentStep];
if (millis() - sysState.stepStartTime >= step.duration) {
sysState.currentStep++;
if (sysState.currentStep < show.stepCount) {
sysState.stepStartTime = millis();
executeStep(sysState.currentStep);
} else {
stopShow();
processQueue();
}
}
}
void stopShow() {
if (!sysState.showRunning) return;
clearAllRelays();
clearFXSegments();
sysState.showRunning = false;
sysState.currentShow = -1;
sysState.currentStep = 0;
Serial.println("Show stopped");
}
// ==================== SCENE SELECTION SYSTEM ====================
void handleDigitEntry(int digit) {
if (sysState.digitIndex < 3) {
sysState.sceneDigits[sysState.digitIndex] = digit;
sysState.digitIndex++;
sysState.lastDigitTime = millis();
if (sysState.digitIndex == 3) {
sysState.sceneReady = true;
int sceneNum = (sysState.sceneDigits[0] * 100) +
(sysState.sceneDigits[1] * 10) +
sysState.sceneDigits[2];
Serial.printf("Scene %03d ready\n", sceneNum);
}
}
}
int getSceneNumber() {
return (sysState.sceneDigits[0] * 100) +
(sysState.sceneDigits[1] * 10) +
sysState.sceneDigits[2];
}
void executeSceneSelection() {
if (!sysState.sceneReady) return;
int sceneNum = getSceneNumber();
if (sceneNum >= 1 && sceneNum <= totalShowCount) {
if (sysState.builderMode) {
addToQueue(sceneNum - 1);
} else {
startShow(sceneNum - 1);
}
} else {
Serial.printf("Invalid scene: %03d\n", sceneNum);
}
// Reset entry
sysState.digitIndex = 0;
sysState.sceneReady = false;
memset(sysState.sceneDigits, 0, sizeof(sysState.sceneDigits));
}
void resetSceneEntry() {
sysState.digitIndex = 0;
sysState.sceneReady = false;
memset(sysState.sceneDigits, 0, sizeof(sysState.sceneDigits));
}
// ==================== QUEUE SYSTEM ====================
void addToQueue(int showIndex) {
if (sysState.queueCount < 10) {
sysState.sceneQueue[sysState.queueCount] = showIndex;
sysState.queueCount++;
Serial.printf("Added to queue: %s (position %d)\n",
showLibrary[showIndex].name, sysState.queueCount);
} else {
Serial.println("Queue full!");
}
}
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;
startShow(sysState.sceneQueue[0]);
Serial.printf("Starting queue: %d shows\n", sysState.queueCount);
}
}
void processQueue() {
if (!sysState.queueMode) return;
sysState.queueIndex++;
if (sysState.queueIndex < sysState.queueCount) {
startShow(sysState.sceneQueue[sysState.queueIndex]);
Serial.printf("Queue progress: %d/%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 ===");
Serial.println("Enter scenes, then press SELECT to start queue");
} else {
Serial.println("Builder mode disabled");
}
}
// ==================== EMERGENCY SYSTEM ====================
void activateEmergency() {
sysState.emergencyActive = true;
sysState.emergencyFlash = true;
sysState.emergencyStart = millis();
stopShow();
// Apply relay emergency behavior
if (settings.killRelaysOnEmergency) {
clearAllRelays();
if (settings.emergencyRelayIndex < 8) {
setRelay(settings.emergencyRelayIndex, true);
}
}
Serial.println("=== EMERGENCY ACTIVATED ===");
}
void clearEmergency() {
sysState.emergencyActive = false;
sysState.emergencyFlash = true;
digitalWrite(STATUS_LED, LOW);
Serial.println("Emergency cleared");
}
void updateEmergencyState() {
if (sysState.emergencyActive) {
// Flash status LED
static uint32_t lastFlash = 0;
if (millis() - lastFlash > 500) {
digitalWrite(STATUS_LED, !digitalRead(STATUS_LED));
lastFlash = millis();
}
// Keep emergency relay behavior active
if (settings.killRelaysOnEmergency) {
for (int i = 0; i < 8; i++) {
if (i == settings.emergencyRelayIndex) {
setRelay(i, true);
} else {
setRelay(i, false);
}
}
}
}
}
// ==================== BUTTON HANDLING ====================
void handleButtonPress(int buttonNum) {
uint32_t now = millis();
// Update activity timestamp
sysState.lastActivity = now;
// Handle settings menu if open
if (menuState.state != MenuState::HIDDEN) {
handleSettingsMenuButton(buttonNum);
return;
}
// Flash indicator
flashButtonIndicator(buttonNum);
// Handle button functions
if (buttonNum >= 0 && buttonNum <= 9) {
// Digit entry
handleDigitEntry(buttonNum);
}
else if (buttonNum == 16) { // START
if (sysState.emergencyActive) {
clearEmergency();
} else {
executeSceneSelection();
}
}
else if (buttonNum == 17) { // RESET
if (sysState.emergencyActive) {
clearEmergency();
} else {
// Short press - reset entry
resetSceneEntry();
clearQueue();
sysState.builderMode = false;
}
}
else if (buttonNum >= 18 && buttonNum <= 25) {
// Manual relay control
int relayIndex = buttonNum - 18;
toggleRelay(relayIndex);
}
else if (buttonNum == 26) { // QUEUE/BUILDER
toggleBuilderMode();
}
else if (buttonNum == 29) { // SELECT
if (sysState.builderMode && sysState.queueCount > 0) {
startQueue();
sysState.builderMode = false;
}
}
else if (buttonNum == 30) { // RELOAD
Serial.println("Reloading shows...");
initBuiltInShows();
loadAllShows();
}
else if (buttonNum == 31) { // EMERGENCY
activateEmergency();
}
}
void processButtons() {
readAllButtons();
uint32_t now = millis();
for (int i = 0; i < 32; i++) {
// Detect button press (rising edge)
if (sysState.buttonStates[i] && !sysState.lastButtonStates[i]) {
if (now - sysState.lastButtonPress[i] > BUTTON_DEBOUNCE_MS) {
sysState.lastButtonPress[i] = now;
sysState.longPressStart[i] = now;
handleButtonPress(i);
}
}
// Detect long press for settings menu (button 17 = RESET)
if (i == 17 && sysState.buttonStates[i] && sysState.longPressStart[i] > 0) {
if (now - sysState.longPressStart[i] > LONG_PRESS_MS &&
menuState.state == MenuState::HIDDEN &&
!sysState.emergencyActive) {
showSettingsMenu();
sysState.longPressStart[i] = 0; // Prevent repeat
}
}
// Clear long press timer on button release
if (!sysState.buttonStates[i]) {
sysState.longPressStart[i] = 0;
}
}
}
// ==================== DISPLAY SYSTEM ====================
void updateIndicatorPixels() {
uint32_t now = millis();
// Working indicator blink
if (now - lastWorkingBlink > (sysState.showRunning ? 250 : 500)) {
workingBlink = !workingBlink;
lastWorkingBlink = now;
}
indicator.setPixelColor(IND_WORKING, workingBlink ? COLOR_BLUE : COLOR_OFF);
// Power indicator
uint32_t powerColor = COLOR_GREEN;
if (sysState.emergencyActive) {
powerColor = COLOR_RED;
} else if (sdAvailable) {
powerColor = COLOR_CYAN;
}
indicator.setPixelColor(IND_POWER, powerColor);
// SD status
indicator.setPixelColor(IND_SD_STATUS, sdAvailable ? COLOR_GREEN : COLOR_RED);
// Emergency status
indicator.setPixelColor(IND_EMERGENCY, sysState.emergencyActive ? COLOR_RED : COLOR_OFF);
// Button flash decay
for (int i = 0; i < 10; i++) {
if (buttonFlashTime[i] > 0) {
if (now - buttonFlashTime[i] > 200) {
buttonFlashTime[i] = 0;
indicator.setPixelColor(IND_BUTTONS[i], COLOR_OFF);
}
}
}
// Relay indicators
for (int i = 0; i < 8; i++) {
indicator.setPixelColor(IND_RELAYS[i], sysState.relayStates[i] ? COLOR_ORANGE : COLOR_OFF);
}
indicator.show();
}
void updateOLEDDisplay() {
if (!oledAvailable) return;
// Settings menu takes priority
if (menuState.state != MenuState::HIDDEN) {
drawSettingsMenu();
return;
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println(F("SHOWDUINO v2.9"));
display.drawLine(0, 9, 127, 9, SSD1306_WHITE);
if (sysState.emergencyActive) {
display.setTextSize(2);
display.setCursor(15, 25);
display.println(F("EMERGENCY"));
display.setCursor(35, 45);
display.println(F("STOP"));
}
else if (sysState.showRunning) {
Show& currentShow = showLibrary[sysState.currentShow];
display.setCursor(0, 15);
if (sysState.queueMode) {
display.printf("QUEUE %d/%d:", sysState.queueIndex + 1, sysState.queueCount);
} else {
display.println(F("RUNNING:"));
}
display.setCursor(0, 25);
display.println(currentShow.name);
if (currentShow.fromSD) {
display.setCursor(0, 35);
display.println(F("(SD Card)"));
}
display.setCursor(0, 50);
display.printf("Step %d/%d | FX: %d",
sysState.currentStep + 1, currentShow.stepCount, activeFXCount);
}
else if (sysState.sceneReady) {
display.setCursor(0, 15);
display.println(F("SCENE READY:"));
display.setTextSize(2);
display.setCursor(25, 30);
display.printf("%03d", getSceneNumber());
display.setTextSize(1);
display.setCursor(15, 50);
display.println(F("Press START"));
}
else if (sysState.digitIndex > 0) {
display.setCursor(0, 15);
display.println(F("Enter Scene:"));
display.setTextSize(2);
display.setCursor(20, 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 if (sysState.builderMode) {
display.setCursor(0, 15);
display.println(F("SCENE BUILDER"));
display.setCursor(0, 25);
display.printf("Queue: %d scenes", sysState.queueCount);
if (sysState.queueCount > 0) {
display.setCursor(0, 35);
display.println("Press SELECT to start");
} else {
display.setCursor(0, 35);
display.println("Enter scenes to queue");
}
display.setCursor(0, 50);
display.println("RESET to exit builder");
}
else {
// Main ready screen
display.setCursor(0, 15);
display.println(F("READY"));
display.setCursor(0, 25);
display.printf("Shows: %d", totalShowCount);
if (sdAvailable) {
int sdCount = 0;
for (int i = 0; i < totalShowCount; i++) {
if (showLibrary[i].fromSD) sdCount++;
}
display.setCursor(0, 35);
display.printf("SD: %d shows", sdCount);
}
display.setCursor(0, 45);
display.printf("FX Lines: %d", NUM_FX_LINES);
display.setCursor(0, 55);
display.println("Hold RESET for settings");
}
display.display();
}
void printSystemStatus() {
if (sysState.emergencyActive) {
Serial.println("[STATUS] EMERGENCY ACTIVE - Press START/RESET to clear");
} else if (sysState.showRunning) {
Show& show = showLibrary[sysState.currentShow];
Serial.printf("[STATUS] Running: %s (Step %d/%d) | FX: %d segments\n",
show.name, sysState.currentStep + 1, show.stepCount, activeFXCount);
} else if (sysState.builderMode) {
Serial.printf("[STATUS] Builder Mode | Queue: %d scenes\n", sysState.queueCount);
} else if (sysState.sceneReady) {
Serial.printf("[STATUS] Scene %03d ready - press START\n", getSceneNumber());
} else {
Serial.printf("[STATUS] Ready | Shows: %d | FX Lines: %d | Triggered: %d\n",
totalShowCount, NUM_FX_LINES, sysState.showsTriggered);
}
}
// ==================== SAFETY AND MAINTENANCE ====================
void performSafetyChecks() {
// Auto-shutoff check
if (settings.autoShutoff &&
(millis() - sysState.lastActivity) > AUTO_SHUTOFF_MS) {
stopShow();
clearQueue();
sysState.lastActivity = millis();
Serial.println("Auto-shutdown triggered");
}
// Scene entry timeout
if (sysState.digitIndex > 0 &&
(millis() - sysState.lastDigitTime) > SCENE_TIMEOUT_MS) {
resetSceneEntry();
Serial.println("Scene entry timeout");
}
// Builder mode timeout (10 minutes)
if (sysState.builderMode &&
(millis() - sysState.lastBuilderActivity) > 600000) {
sysState.builderMode = false;
Serial.println("Builder mode timeout");
}
}
void performSystemMaintenance() {
static uint32_t lastMaintenance = 0;
if (millis() - lastMaintenance > 60000) { // Every minute
// Update EEPROM wear leveling
static uint8_t maintenanceCounter = 0;
maintenanceCounter++;
if (maintenanceCounter % 10 == 0) {
// Periodic EEPROM save (every 10 minutes)
saveSettingsToEEPROM();
}
// Memory status
Serial.printf("[MAINTENANCE] Free heap: %d bytes\n", ESP.getFreeHeap());
lastMaintenance = millis();
}
}
// ==================== MAIN SYSTEM FUNCTIONS ====================
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("===============================================");
Serial.println("SHOWDUINO ESP32-S3 v2.9 COMPLETE");
Serial.println("Professional Show Controller with FX Engine");
Serial.println("===============================================");
// Initialize hardware
initPins();
initEEPROM();
// Initialize displays and pixels
oledAvailable = initOLED();
initIndicatorPixels();
initFXPixels();
// Initialize SD card
sdAvailable = initSD();
if (sdAvailable) {
createDirectoryStructure();
createExampleFiles();
// Try to load settings from SD
if (SD.exists("/config/settings.txt")) {
loadSettingsFromSD();
Serial.println("Settings loaded from SD");
}
}
// Initialize shows
initBuiltInShows();
if (sdAvailable) {
loadAllShows();
}
// Initialize state
sysState.lastActivity = millis();
sysState.lastDigitTime = millis();
sysState.lastBuilderActivity = millis();
// System ready
Serial.println();
Serial.println("=== SYSTEM READY ===");
Serial.printf("Total Shows: %d\n", totalShowCount);
Serial.printf("FX Lines: %d\n", NUM_FX_LINES);
Serial.printf("SD Card: %s\n", sdAvailable ? "Available" : "Not found");
Serial.printf("OLED: %s\n", oledAvailable ? "Available" : "Not found");
Serial.println();
Serial.println("CONTROLS:");
Serial.println(" 0-9: Scene digits");
Serial.println(" 16: START | 17: RESET (hold for settings)");
Serial.println(" 18-25: Manual relay toggles");
Serial.println(" 26: Builder mode | 29: Execute queue");
Serial.println(" 30: Reload SD | 31: EMERGENCY STOP");
Serial.println();
Serial.println("ENHANCED FEATURES:");
Serial.println(" - Multi-line FX system with per-pixel segments");
Serial.println(" - Advanced effects: solid, flash, chase, twinkle, gradient, wave");
Serial.println(" - Settings menu with EEPROM + SD persistence");
Serial.println(" - Configurable emergency behavior");
Serial.println(" - Scene builder and queue system");
Serial.println();
if (oledAvailable) {
display.clearDisplay();
display.setCursor(0, 0);
display.println(F("SHOWDUINO v2.9"));
display.println(F("System Ready"));
display.printf("Shows: %d\n", totalShowCount);
display.printf("FX Lines: %d\n", NUM_FX_LINES);
display.println(F("Hold RESET for settings"));
display.display();
}
}
void loop() {
// Core system updates
processButtons();
updateShowProgress();
updateEmergencyState();
updateFXSystem();
// Display updates (throttled)
static uint32_t lastDisplayUpdate = 0;
if (millis() - lastDisplayUpdate > DISPLAY_UPDATE_MS) {
updateIndicatorPixels();
updateOLEDDisplay();
lastDisplayUpdate = millis();
}
// Status updates (throttled)
static uint32_t lastStatusUpdate = 0;
if (millis() - lastStatusUpdate > 2000) { // Every 2 seconds
printSystemStatus();
lastStatusUpdate = millis();
}
// Safety and maintenance
performSafetyChecks();
performSystemMaintenance();
// Main loop delay
delay(5);
}
/* ===============================================
* END OF SHOWDUINO v2.9 COMPLETE
*
* This is a comprehensive show control system featuring:
*
* HARDWARE SUPPORT:
* - ESP32-S3 with 32 multiplexed buttons
* - 8 relay outputs
* - SSD1306 OLED display
* - SD card storage
* - 1 indicator NeoPixel strip
* - 4 dedicated FX NeoPixel strips
*
* SOFTWARE FEATURES:
* - Advanced FX engine with 6+ effects
* - Per-pixel segment control
* - SD-based show and FX definitions
* - Interactive settings menu
* - Scene builder and queue system
* - Emergency management with configurable behavior
* - EEPROM + SD settings persistence
* - Professional safety features
*
* USAGE:
* 1. Flash to ESP32-S3
* 2. Connect hardware per pin definitions
* 3. Insert SD card with show/FX files
* 4. Use buttons to control shows
* 5. Hold RESET for settings menu
*
* For more information and examples, see the
* ChatGPT conversation this was built from.
* =============================================== */