/**
* SHOWDUINO ESP32-S3 PROFESSIONAL SHOW CONTROLLER v2.9 - FX ENGINE EDITION
* Based on v2.8 Fast SD Version + ChatGPT FX Engine Roadmap
*
* NEW FEATURES:
* - 4 dedicated FX NeoPixel lines with per-pixel segment control
* - SD-based FX definition system (/fx/*.txt files)
* - Enhanced settings menu with emergency relay options
* - Layered FX priority system with color blending
* - Advanced effects: solid, flash, chase, twinkle, gradient, wave, fade
*/
#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>
// ==================== PIN CONFIGURATION ====================
// MUX control pins (from v2.8 - unchanged)
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 // Indicator NeoPixel data pin
#define NEOPIXEL_COUNT 30 // Total number of indicator NeoPixels
// NEW: FX NeoPixel Lines (4 dedicated FX strips)
#define NUM_FX_LINES 4
const int fxPins[NUM_FX_LINES] = {38, 39, 40, 41};
const int fxLengths[NUM_FX_LINES] = {30, 30, 24, 16};
// OLED Display (I2C)
#define OLED_SDA 9 // I2C Data
#define OLED_SCL 10 // I2C Clock
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
// SD Card SPI pins
#define SD_CS_PIN 47 // Chip Select
#define SD_MOSI_PIN 35 // MOSI
#define SD_MISO_PIN 37 // MISO
#define SD_SCK_PIN 36 // Clock
// ==================== 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
#define MAX_SD_SHOWS 50
#define MAX_STEPS_PER_SHOW 20
#define MAX_FX_SEGMENTS 32 // NEW: Max FX segments
// ==================== ENHANCED SHOW FORMAT ====================
struct FastShowStep {
uint16_t duration; // milliseconds
uint8_t relayMask; // 8-bit relay pattern
char fxFile[32]; // NEW: optional FX file for this step
};
struct FastShow {
char name[32]; // Fixed-size string
uint8_t stepCount;
uint16_t totalDuration;
FastShowStep steps[MAX_STEPS_PER_SHOW];
bool fromSD;
};
// ==================== NEW: FX SYSTEM STRUCTURES ====================
struct FXSegment {
uint8_t line; // which FX line (0..3)
uint8_t start; // start pixel
uint8_t length; // number of pixels
uint8_t effectID; // effect type
uint32_t color; // packed RGB
uint16_t speed; // timing param (ms)
uint8_t brightness; // 0-255
uint16_t fade; // fade time (ms)
uint8_t amplitude; // wave amplitude
uint32_t colors[4]; // gradient colors (up to 4 stops)
uint8_t colorCount; // how many gradient stops
uint8_t priority; // layer priority for blending
};
FXSegment activeFXSegments[MAX_FX_SEGMENTS];
uint8_t activeFXSegmentCount = 0;
// ==================== GLOBALS ====================
struct SystemState {
bool relayStates[8] = {false};
uint32_t relayOnTime[8] = {0};
bool emergencyActive = false;
bool emergencyFlash = true; // NEW: emergency flash stage
uint32_t emergencyStart = 0; // NEW: when emergency started
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;
// ENHANCED: Settings with emergency options
struct ShowSettings {
uint16_t magic = 0xABCD;
bool autoShutoff = true;
uint8_t maxShowDuration = 5;
bool enableSafetyLimits = true;
bool killRelaysOnEmergency = true; // NEW: kill relays on emergency
uint8_t emergencyRelayIndex = 255; // NEW: 255=None, else 0..7
} settings;
// Show library - pre-allocated static array
FastShow showLibrary[85]; // 35 built-in + 50 SD shows
uint8_t totalShowCount = 0;
bool sdCardAvailable = false;
// ==================== OLED + NeoPixel Instances ====================
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
bool oledAvailable = false;
// Indicator NeoPixel (existing functionality preserved)
Adafruit_NeoPixel strip(NEOPIXEL_COUNT, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
// NEW: FX NeoPixel lines
Adafruit_NeoPixel fxStrips[NUM_FX_LINES] = {
Adafruit_NeoPixel(fxLengths[0], fxPins[0], NEO_GRB + NEO_KHZ800),
Adafruit_NeoPixel(fxLengths[1], fxPins[1], NEO_GRB + NEO_KHZ800),
Adafruit_NeoPixel(fxLengths[2], fxPins[2], NEO_GRB + NEO_KHZ800),
Adafruit_NeoPixel(fxLengths[3], fxPins[3], NEO_GRB + NEO_KHZ800)
};
// ==================== Colors & NeoPixel Mapping (preserved) ====================
#define COLOR_OFF 0x000000
#define COLOR_GREEN 0x00FF00
#define COLOR_RED 0xFF0000
#define COLOR_BLUE 0x0000FF
#define COLOR_CYAN 0x00FFFF
#define COLOR_WHITE 0xFFFFFF
#define COLOR_ORANGE 0xFF8000
constexpr int NEO_IDX_POWER = 0;
constexpr int NEO_IDX_WORKING = 1;
int NEO_INDEX_BUTTON[10] = {11, 4, 3, 2, 7, 6, 5, 10, 9, 8};
int NEO_INDEX_RELAY[8] = {14, 15, 16, 17, 18, 19, 20, 21};
uint32_t btnFlashAt[10] = {0};
bool workBlink = false;
uint32_t lastWorkBlink = 0;
// ==================== NEW: FX ENGINE FUNCTIONS ====================
void initFX() {
for (int i = 0; i < NUM_FX_LINES; i++) {
fxStrips[i].begin();
fxStrips[i].clear();
fxStrips[i].setBrightness(128);
fxStrips[i].show();
}
Serial.println("FX NeoPixel lines initialized");
}
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 blend (clamped)
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;
}
uint8_t applyFade(uint8_t value, FXSegment &seg) {
if (seg.fade == 0) return value;
// Fade factor oscillates between 0..1 over fade ms
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);
}
uint32_t adjustColor(uint32_t c, FXSegment &seg) {
uint8_t r = (c >> 16) & 0xFF;
uint8_t g = (c >> 8) & 0xFF;
uint8_t b = (c ) & 0xFF;
// Apply brightness scaling
r = (r * seg.brightness) / 255;
g = (g * seg.brightness) / 255;
b = (b * seg.brightness) / 255;
// Apply fade modulation
r = applyFade(r, seg);
g = applyFade(g, seg);
b = applyFade(b, seg);
return fxStrips[seg.line].Color(r, g, b);
}
void applyFXEffect(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 c = (p == pos) ? adjustColor(seg.color, seg) : 0;
strip.setPixelColor(seg.start + p, c);
}
}
break;
case 3: // twinkle
for (int p = seg.start; p < seg.start + seg.length; p++) {
if (random(1000) < 5) {
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;
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 sineVal = (sin(phase * 0.1f) + 1.0f) * 0.5f; // 0..1
uint8_t r = ((seg.color >> 16) & 0xFF) * sineVal;
uint8_t g = ((seg.color >> 8) & 0xFF) * sineVal;
uint8_t b = ((seg.color ) & 0xFF) * sineVal;
uint32_t waveColor = strip.Color(r, g, b);
strip.setPixelColor(seg.start + i, adjustColor(waveColor, seg));
}
break;
}
}
void updateFX() {
// Clear all FX strips first
for (int i = 0; i < NUM_FX_LINES; i++) {
fxStrips[i].clear();
}
// Apply each active segment (layered)
for (int s = 0; s < activeFXSegmentCount; s++) {
applyFXEffect(activeFXSegments[s]);
}
// Apply emergency override if active
applyEmergencyFX();
// Update all FX strips
for (int i = 0; i < NUM_FX_LINES; i++) {
fxStrips[i].show();
}
}
void applyEmergencyFX() {
if (!sysState.emergencyActive) return;
uint32_t now = millis();
// Stage 1: 2s red flashing
if (sysState.emergencyFlash && (now - sysState.emergencyStart < 2000)) {
bool on = ((now / 250) % 2 == 0); // 4Hz flash
for (int l = 0; l < NUM_FX_LINES; l++) {
for (int p = 0; p < fxLengths[l]; p++) {
fxStrips[l].setPixelColor(p, on ? fxStrips[l].Color(255, 0, 0) : 0);
}
}
}
// Stage 2: solid white emergency lighting
else {
sysState.emergencyFlash = false;
for (int l = 0; l < NUM_FX_LINES; l++) {
for (int p = 0; p < fxLengths[l]; p++) {
fxStrips[l].setPixelColor(p, fxStrips[l].Color(255, 255, 255));
}
}
}
}
void clearAllFX() {
activeFXSegmentCount = 0;
for (int i = 0; i < NUM_FX_LINES; i++) {
fxStrips[i].clear();
fxStrips[i].show();
}
}
void parseFXFile(const char* filename) {
if (!sdCardAvailable) return;
String fullPath = "/fx/" + String(filename);
File f = SD.open(fullPath);
if (!f) {
Serial.printf("FX file not found: %s\n", fullPath.c_str());
return;
}
activeFXSegmentCount = 0;
while (f.available() && activeFXSegmentCount < MAX_FX_SEGMENTS) {
String line = f.readStringUntil('\n');
line.trim();
if (!line.startsWith("FX:")) continue;
FXSegment seg;
seg.speed = 200; // defaults
seg.brightness = 255;
seg.fade = 0;
seg.amplitude = 0;
seg.colorCount = 0;
seg.priority = 1;
// Parse: FX: line0 0-3 solid color=255,0,0 speed=500
int lineIdx, start, end, r = 255, g = 0, b = 0, speed = 200;
char effect[16];
// Basic parsing - can be enhanced for full key=value support
sscanf(line.c_str(), "FX: line%d %d-%d %15s %d,%d,%d %d",
&lineIdx, &start, &end, effect, &r, &g, &b, &speed);
if (lineIdx >= NUM_FX_LINES) continue;
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 to IDs
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;
else seg.effectID = 0;
activeFXSegments[activeFXSegmentCount++] = seg;
}
f.close();
Serial.printf("Loaded %d FX segments from %s\n", activeFXSegmentCount, filename);
}
// ==================== ENHANCED SHOW FUNCTIONS ====================
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;
}
bool loadShowFromSD(const char* filename) {
if (totalShowCount >= 85) return false;
String fullPath = "/shows/" + String(filename);
File file = SD.open(fullPath);
if (!file) return false;
FastShow* show = &showLibrary[totalShowCount];
show->fromSD = true;
show->stepCount = 0;
show->totalDuration = 0;
// Default name from filename
String nameStr = String(filename);
nameStr.replace(".txt", "");
strncpy(show->name, nameStr.c_str(), 31);
show->name[31] = 0;
// Parse file line by line - ENHANCED for FX support
while (file.available() && show->stepCount < MAX_STEPS_PER_SHOW) {
String line = file.readStringUntil('\n');
line.trim();
if (line.startsWith("#")) {
// Parse name from comment
if (line.startsWith("# Name:")) {
String name = line.substring(7);
name.trim();
strncpy(show->name, name.c_str(), 31);
show->name[31] = 0;
}
continue;
}
if (line.length() < 10) continue;
int firstComma = line.indexOf(',');
int secondComma = line.indexOf(',', firstComma + 1);
if (firstComma > 0) {
uint16_t duration = line.substring(0, firstComma).toInt();
String maskStr = line.substring(firstComma + 1, secondComma > 0 ? secondComma : line.length());
maskStr.trim();
if (duration > 0 && maskStr.length() >= 8) {
FastShowStep* step = &show->steps[show->stepCount];
step->duration = duration;
step->relayMask = parseBinary8(maskStr.c_str());
// NEW: Parse FX file if present
if (secondComma > 0) {
String fxFile = line.substring(secondComma + 1);
fxFile.trim();
strncpy(step->fxFile, fxFile.c_str(), sizeof(step->fxFile) - 1);
step->fxFile[31] = 0;
} else {
step->fxFile[0] = '\0';
}
show->totalDuration += duration;
show->stepCount++;
}
}
}
file.close();
if (show->stepCount > 0) {
Serial.printf("Loaded: %s (%d steps)\n", show->name, show->stepCount);
totalShowCount++;
return true;
}
return false;
}
void executeShowStep(int stepIndex) {
FastShow* show = &showLibrary[sysState.currentShow];
FastShowStep* step = &show->steps[stepIndex];
// Relay control (existing)
for (int i = 0; i < 8; i++) {
bool on = (step->relayMask >> (7-i)) & 1;
setRelay(i, on);
}
// NEW: FX control
if (strlen(step->fxFile) > 0) {
parseFXFile(step->fxFile);
Serial.printf("FX loaded: %s\n", step->fxFile);
} else {
clearAllFX(); // Clear FX if no file specified
}
}
// ==================== PRESERVED v2.8 FUNCTIONS ====================
// [All existing functions from v2.8 preserved - relay control, buttons, display, etc.]
void initBuiltInShows() {
totalShowCount = 0;
// Scene 001: Relay 1 Only
strncpy(showLibrary[totalShowCount].name, "Scene 001", 31);
showLibrary[totalShowCount].stepCount = 2;
showLibrary[totalShowCount].steps[0] = {3000, 0b00000001, ""};
showLibrary[totalShowCount].steps[1] = {1000, 0b00000000, ""};
showLibrary[totalShowCount].totalDuration = 4000;
showLibrary[totalShowCount].fromSD = false;
totalShowCount++;
// Add more built-in shows with empty FX files...
// [Abbreviated for space - would include all original scenes]
Serial.printf("Built-in shows loaded: %d\n", totalShowCount);
}
bool initSDCard() {
Serial.println("Initializing 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;
}
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
Serial.printf("SD Card Ready: %lluMB\n", cardSize);
return true;
}
void createExampleFiles() {
if (!SD.exists("/shows")) {
SD.mkdir("/shows");
}
// NEW: Create FX directory and example files
if (!SD.exists("/fx")) {
SD.mkdir("/fx");
}
// Enhanced show format example with FX
File txtFile = SD.open("/shows/fx_example.txt", FILE_WRITE);
if (txtFile) {
txtFile.println("# SHOWDUINO Enhanced Show Format with FX");
txtFile.println("# Name: FX Demo Show");
txtFile.println("# Format: duration_ms,relay_mask_binary,fx_file");
txtFile.println("2000,00000001,scene1.txt"); // 2 seconds, relay 1, load scene1.txt FX
txtFile.println("3000,00000010,scene2.txt"); // 3 seconds, relay 2, load scene2.txt FX
txtFile.println("1000,00000000"); // 1 second, all off, no FX
txtFile.close();
Serial.println("Created fx_example.txt");
}
// Example FX files
File fxFile1 = SD.open("/fx/scene1.txt", FILE_WRITE);
if (fxFile1) {
fxFile1.println("# Simple red solid + green flash");
fxFile1.println("FX: line0 0-15 solid 255,0,0");
fxFile1.println("FX: line1 0-10 flash 0,255,0 500");
fxFile1.close();
Serial.println("Created /fx/scene1.txt");
}
File fxFile2 = SD.open("/fx/scene2.txt", FILE_WRITE);
if (fxFile2) {
fxFile2.println("# Chase and twinkle demo");
fxFile2.println("FX: line0 0-20 chase 0,0,255 150");
fxFile2.println("FX: line2 0-15 twinkle 255,255,255 200");
fxFile2.close();
Serial.println("Created /fx/scene2.txt");
}
}
// [All other v2.8 functions preserved but adjusted for FX integration...]
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);
}
}
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 triggerShow(int idx) {
if (idx >= totalShowCount) return;
if (sysState.showRunning) stopShow();
sysState.currentShow = idx;
sysState.showRunning = true;
sysState.showStartTime = millis();
sysState.showsTriggered++;
// Execute first step immediately (now with FX support)
executeShowStep(0);
FastShow* show = &showLibrary[idx];
Serial.printf("Show started: %s %s\n", show->name, show->fromSD ? "(SD)" : "");
}
void updateShow() {
static int currentStep = 0;
static uint32_t stepStart = 0;
if (stepStart == 0) stepStart = millis();
FastShow* show = &showLibrary[sysState.currentShow];
if (millis() - stepStart >= show->steps[currentStep].duration) {
currentStep++;
if (currentStep < show->stepCount) {
executeShowStep(currentStep);
stepStart = millis();
} else {
stopShow();
currentStep = 0;
stepStart = 0;
updateQueue();
}
}
}
void stopShow() {
for (int i = 0; i < 8; i++) setRelay(i, false);
clearAllFX(); // NEW: Clear FX when stopping show
sysState.showRunning = false;
sysState.currentShow = -1;
Serial.println("Show stopped.");
}
// ==================== ENHANCED EMERGENCY HANDLING ====================
void handleEmergencyMode() {
if (sysState.emergencyActive) {
// Apply relay control based on settings
if (settings.killRelaysOnEmergency) {
for (int i = 0; i < 8; i++) {
if (i == settings.emergencyRelayIndex) {
setRelay(i, true); // Keep emergency relay ON
} else {
setRelay(i, false); // Kill all others
}
}
}
static uint32_t lastFlash = 0;
if (millis() - lastFlash > 500) {
digitalWrite(STATUS_LED_PIN, !digitalRead(STATUS_LED_PIN));
lastFlash = millis();
}
} else {
digitalWrite(STATUS_LED_PIN, LOW);
}
}
// ==================== REMAINING v2.8 FUNCTIONS ====================
void handleSceneEntry(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 - press START\n", sceneNum);
}
}
}
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 <= totalShowCount) {
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 (max: %d)\n", sceneNum, totalShowCount);
sysState.digitIndex = 0;
sysState.sceneReady = false;
memset(sysState.sceneDigits, 0, sizeof(sysState.sceneDigits));
}
}
}
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);
}
}
void toggleRelay(int relayIndex) {
bool newState = !sysState.relayStates[relayIndex];
setRelay(relayIndex, newState);
Serial.printf("Manual relay %d: %s\n", relayIndex + 1, newState ? "ON" : "OFF");
}
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;
showButtonPress(i);
if (i >= 0 && i <= 9) {
handleSceneEntry(i);
}
else if (i == 31) {
Serial.println("=== EMERGENCY STOP ACTIVATED ===");
sysState.emergencyActive = true;
sysState.emergencyFlash = true;
sysState.emergencyStart = millis();
stopShow();
}
else if (i == 16) {
if (sysState.emergencyActive) {
sysState.emergencyActive = false;
sysState.emergencyFlash = true;
digitalWrite(STATUS_LED_PIN, LOW);
Serial.println("Emergency cleared via START");
} else {
executeSceneSelection();
}
}
else if (i == 17) {
if (sysState.emergencyActive) {
sysState.emergencyActive = false;
sysState.emergencyFlash = true;
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;
if (relayIdx < 8) {
toggleRelay(relayIdx);
}
}
else if (i == 26) {
toggleBuilderMode();
}
else if (i == 27) {
if (sysState.builderMode) {
navigateQueue(true);
}
}
else if (i == 28) {
if (sysState.builderMode) {
navigateQueue(false);
}
}
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 if (i == 30) {
Serial.println("RELOAD SD button pressed");
uint8_t oldCount = totalShowCount;
initBuiltInShows();
loadAllSDShows();
Serial.printf("Reloaded: %d shows (%d new)\n", totalShowCount, totalShowCount - oldCount);
}
}
}
}
}
void loadAllSDShows() {
if (!sdCardAvailable) return;
Serial.println("Loading SD shows...");
uint32_t startTime = millis();
File root = SD.open("/shows");
if (!root) return;
int sdCount = 0;
File file = root.openNextFile();
while (file && totalShowCount < 85) {
if (!file.isDirectory()) {
String filename = file.name();
if (filename.endsWith(".txt")) {
if (loadShowFromSD(filename.c_str())) {
sdCount++;
}
}
}
file = root.openNextFile();
}
root.close();
uint32_t loadTime = millis() - startTime;
Serial.printf("Loaded %d SD shows in %dms\n", sdCount, loadTime);
}
void updateDisplay() {
if (sysState.emergencyActive) {
Serial.println("[DISPLAY] ⚠️ EMERGENCY STOP ACTIVE - Press START/RESET to clear");
} else if (sysState.showRunning) {
FastShow* currentShow = &showLibrary[sysState.currentShow];
if (sysState.queueMode) {
Serial.printf("[DISPLAY] Queue %d/%d: %s %s\n",
sysState.queueIndex + 1, sysState.queueCount,
currentShow->name,
currentShow->fromSD ? "(SD)" : "");
} else {
Serial.printf("[DISPLAY] Running: %s %s\n",
currentShow->name,
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 < totalShowCount) {
Serial.printf(" | Cursor: %s", showLibrary[sceneIdx].name);
}
}
Serial.println();
} else if (sysState.sceneReady) {
int sceneNum = getSceneNumber();
if (sceneNum <= totalShowCount) {
Serial.printf("[DISPLAY] Ready: %s - press START\n",
showLibrary[sceneNum - 1].name);
} 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 {
uint8_t builtInCount = 0;
for (int i = 0; i < totalShowCount; i++) {
if (!showLibrary[i].fromSD) builtInCount++;
}
Serial.printf("[DISPLAY] Ready | Shows: %d (%d SD) | Triggered: %d | FX: %d segments",
totalShowCount,
totalShowCount - builtInCount,
sysState.showsTriggered,
activeFXSegmentCount);
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.9"));
display.println(F("FX Engine Edition"));
display.println(F("Show Controller"));
display.println();
display.printf("SD Card: %s", sdCardAvailable ? "Ready" : "None");
display.printf("FX Lines: %d", NUM_FX_LINES);
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.9"));
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) {
FastShow* currentShow = &showLibrary[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);
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);
if (currentShow->fromSD) {
display.setCursor(0, 35);
display.println(F("(SD Card)"));
}
}
display.setCursor(0, 50);
display.print(F("FX: "));
display.print(activeFXSegmentCount);
display.print(F(" segments"));
}
else {
display.setCursor(0, 15);
display.println(F("READY"));
display.setCursor(0, 25);
display.printf("Shows: %d", totalShowCount);
uint8_t sdShowCount = 0;
for (int i = 0; i < totalShowCount; i++) {
if (showLibrary[i].fromSD) sdShowCount++;
}
if (sdCardAvailable && sdShowCount > 0) {
display.setCursor(0, 35);
display.printf("SD: %d shows", sdShowCount);
} else {
display.setCursor(0, 35);
display.println(F("Enter 3-digit scene"));
}
display.setCursor(0, 50);
display.printf("FX Lines: %d", NUM_FX_LINES);
}
display.display();
}
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);
}
void initNeoPixels() {
// Initialize indicator strip (existing functionality)
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();
Serial.println("Indicator NeoPixels initialized");
}
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);
uint32_t powerColor = COLOR_GREEN;
if (sysState.emergencyActive) {
powerColor = COLOR_RED;
} else if (sdCardAvailable) {
powerColor = COLOR_CYAN;
}
setPixelHex(NEO_IDX_POWER, powerColor);
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 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");
}
}
// ==================== ENHANCED SETUP ====================
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("=== SHOWDUINO ESP32-S3 FX ENGINE v2.9 ===");
Serial.println("Enhanced with multi-line FX system");
EEPROM.begin(EEPROM_SIZE);
loadSettings();
// Initialize built-in shows first
initBuiltInShows();
// Initialize SD card
sdCardAvailable = initSDCard();
// Relays
for (int i = 0; i < 8; i++) {
pinMode(relayPins[i], OUTPUT);
digitalWrite(relayPins[i], LOW);
}
// MUX control
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);
// Initialize all display and pixel systems
initOLED();
initNeoPixels();
initFX(); // NEW: Initialize FX NeoPixel lines
// SD Card setup
if (sdCardAvailable) {
createExampleFiles();
loadAllSDShows();
}
sysState.lastActivity = millis();
sysState.lastDigitTime = millis();
Serial.printf("Total shows available: %d\n", totalShowCount);
Serial.printf("SD Card: %s\n", sdCardAvailable ? "Available" : "Not found");
Serial.printf("FX Lines: %d\n", NUM_FX_LINES);
Serial.printf("Emergency relay kill: %s\n", settings.killRelaysOnEmergency ? "Enabled" : "Disabled");
if (settings.emergencyRelayIndex < 8) {
Serial.printf("Emergency relay preserve: Relay %d\n", settings.emergencyRelayIndex + 1);
}
Serial.println();
Serial.println("=== FX ENGINE READY ===");
Serial.println("Enhanced show format: duration_ms,relay_binary,fx_file");
Serial.println("Example: 2000,11111111,scene1.txt");
Serial.println();
}
// ==================== ENHANCED MAIN LOOP ====================
void loop() {
readButtons();
handleEmergencyMode();
if (sysState.emergencyActive) {
handleButtonPresses(); // Still handle emergency clear buttons
updateDisplay();
if (oledAvailable) updateOLEDDisplay();
updateFX(); // Emergency FX override
updateAllNeoPixels(); // Indicator strip
delay(100);
return;
}
handleButtonPresses();
if (sysState.showRunning) updateShow();
static uint32_t lastDisp = 0;
if (millis() - lastDisp > DISPLAY_UPDATE_MS) {
updateDisplay();
if (oledAvailable) updateOLEDDisplay();
updateAllNeoPixels(); // Indicator strip
lastDisp = millis();
}
// NEW: Update FX system continuously
updateFX();
performSafetyChecks();
delay(5);
}
Loading
esp32-s3-devkitc-1
esp32-s3-devkitc-1
Loading
cd74hc4067
cd74hc4067
Loading
cd74hc4067
cd74hc4067