// This is a heavily modified version of this project: https://www.instructables.com/Arduino-based-8-loops-pedal-switcher/
// Information about photoFET muting can be found here: https://stompville.co.uk/?p=423
// You can follow the journey from the instructable to this final solution via the Arduino thread: https://forum.arduino.cc/t/effect-switcher-and-relays-eeprom-question/1209660/165
// There is a working simulation (wokwi) here: https://wokwi.com/projects/389032204915078145 - the red/blue LEDs represent optocouplers that power the relays
// Any diagrams or relevant coding are in the Arduino thread, including power management and .json file for creating your own Wokwi simulation
// Special thanks to Alto777 and "she who will not be named" on the Arduino forum for the teaching and coding and solving and all of the things
# include <EEPROM.h>
# include <LiquidCrystal.h>
# include <Wire.h>
# include <Keypad.h>
# include <ezButton.h>
# include <Adafruit_NeoPixel.h>
unsigned long now; // global current m i l l i s ( ) value for all processes
void okDelay(unsigned long theDelay) { // use this instead of delay() - mostly for setup or short pulses.
delay(theDelay);
}
enum escreen { // list of different LCD display screens
DUMBBITCH = 101, READPRESET, WRITEOUT, RUN, SAVED, PROGRAMMODE, SAVEMODE, SPLASH, NOSCREEN
};
const char *screenTag[] = {
"DUMBBITCH", "READPRESET", "WRITEOUT", "RUN", "SAVED", "PROGRAMMODE", "SAVEMODE", "SPLASH", "NO_SCREEN"
};
ezButton ezBankButton(A6), ezProgButton(A7), ezSaveButton(A9), ezDownButton(A11);
LiquidCrystal lcd(A5, A4, A3, A2, A1, A0);
// setup of neopixels
# define ELEDS 16 // how many
# define EPIN 12 // which pin
# define PLEDS 8
# define PPIN 13
# define SLEDS 1
# define SPIN A15
Adafruit_NeoPixel eLED(ELEDS, EPIN, NEO_RGB + NEO_KHZ800); // effect LEDs
Adafruit_NeoPixel pLED(PLEDS, PPIN, NEO_RGB + NEO_KHZ800); // preset LEDs
Adafruit_NeoPixel sLED(SLEDS, SPIN, NEO_RGB + NEO_KHZ800); // program/save LED
# define rHot HIGH
# define rGnd LOW
const byte rows = 1;
const byte cols = 8; // number of presets - up to 16
char keys[rows][cols] = {
{ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h' }, // max 16 columns and max 10 rows. Ensure to match the numbers in const byte rows/cols above
};
const int numberOfPedals = 16; // your total number of effects/pedals
const int bankSize = numberOfPedals * numberOfPedals; // bank variable to skip ahead by a full bank
const int numberOfBanks = 9; // up to 14 banks @ 16 pedals; 27 banks @ 12 pedals, 62 banks @ 8 pedals - be sure to adjust LCD displays to accommodate double digit bank readout
const int numberOfEfBks = 2; // no more than 2 effect banks - for use ONLY when you have HALF the number of footswitches compared with the number of effects
const int numberOfPhotos = 2; // photoFET variable to mute signal during switching
const int effectVariable = 8; // effect variable for the effect banks to pull proper keys
const int numberOfProgNeos = 1; // program/save LED variable
byte colPins[cols] = { 4, 5, 6, 7, 8, 9, 10, 11 }; // buttons or momentary switches
byte rowPins[rows] = { 2 }; // just 1 row is needed
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, rows, cols);
const int OneRelayPin[numberOfPedals] = { 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37 }; // pin 1 on relay - reset/default has pin 1 at 0v - RED LED
const int TenRelayPin[numberOfPedals] = { 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53 }; // pin 10 on relay - reset/default has pin 10 at 5v - BLUE LED
const int photoPin[numberOfPhotos] = { A12, A13 };
byte midiChannel = 0;
int currentBank = 1; // Starting Preset Bank Number
int effectBank = 1; // Starting Effect Bank Number
enum estate {READ = 0, WRITE, ERROR, SAVE};
const char *modeTag[] = {"READ", "WRITE", "ERROR", "SAVE"};
const byte NOKEY = 99;
// These reset things. Named obvioulsy.
void progLampsOff() {
for (int ii = 0; ii < numberOfPedals; ii++) {
setPresetLED(ii, HIGH);
}
}
void allLampsOff() {
for (int ii = 0; ii < numberOfPedals; ii++) {
setPresetLED(ii, HIGH);
setEffectLED(ii, HIGH);
sLED.clear();
}
}
// save your effect/loop selections to a preset
void memory(int theAddr) {
for (int ii = 0; ii < numberOfPedals; ii++) {
EEPROM.write((theAddr) + ii, getEffectLED(ii));
setPresetLED(ii, HIGH);
}
for (int ii = 0; ii < numberOfPedals; ii++) {
setEffectLED(ii, HIGH);
}
}
// select your effects/loops for saving
void writeOut(int relay) {
progLampsOff();
setEffectLED(relay, getEffectLED(relay) ? LOW : HIGH);
}
// default mode - USE THE SWITCH!
void eepromToRelays(int address) {
for (int ii = 0; ii < numberOfPhotos; ii++) {
digitalWrite(photoPin[ii], rHot); //turn ON all photofets
}
okDelay(4); // FRONT mute to
for (int ii = 0; ii < numberOfPedals; ii++) {
bool onOff = EEPROM.read((address) + ii) ? HIGH : LOW;
if (onOff) {
digitalWrite(OneRelayPin[ii], rGnd);
digitalWrite(TenRelayPin[ii], rHot);
} else {
digitalWrite(TenRelayPin[ii], rGnd);
digitalWrite(OneRelayPin[ii], rHot);
}
int kPreset = EEPROM.read((address) + ii);
setEffectLED(ii, kPreset);
setPresetLED(ii, HIGH);
}
okDelay(5); // + BACK, if
for (int ii = 0; ii < numberOfPedals; ii++) {
digitalWrite(OneRelayPin[ii], rGnd);
digitalWrite(TenRelayPin[ii], rGnd);
}
okDelay(1);
for (int ii = 0; ii < numberOfPhotos; ii++) {
digitalWrite(photoPin[ii], rGnd); //turn OFF all photofets last step
}
}
void readPreset(int address) {
eepromToRelays(address);
}
void loop() {
static byte realMode = READ;
now = millis(); // with zero delays, now is now is now for eveyone
ezBankButton.loop();
ezProgButton.loop();
ezSaveButton.loop();
ezDownButton.loop();
char key = keypad.getKey();
if (!key) {
key = NOKEY;
} else {
key -= 'a';
if (effectBank == 2) { // effectBank 1 is effects 1 through 8, effectBank 2 is 9 through 16
key += effectVariable;
}
}
bool doBank = ezBankButton.isPressed();
bool doProg = ezProgButton.isPressed();
bool doSave = ezSaveButton.isPressed();
bool doDown = ezDownButton.isPressed();
bool anyInput = doBank || doProg || doSave || doDown || key != NOKEY;
serviceLCD(anyInput); // manage transient screens. time or user activity moves us along
runAckBlinker();
switch (realMode) {
case WRITE:
progPresetLED();
if (doBank) {
effectBank = 2;
Serial.print("effect bank became "); Serial.println(effectBank);
paintLCD(WRITEOUT, 0, 103);
}
if (doDown) {
effectBank = 1;
Serial.print("effect bank became "); Serial.println(effectBank);
paintLCD(WRITEOUT, 0, 103);
}
if (doProg) {
realMode = READ;
allLampsOff();
paintLCD(RUN, 0, 103);
}
if (doSave) {
realMode = SAVE;
alreadyStored();
paintLCD(SAVEMODE);
}
if (key != NOKEY) {
writeOut(key); // relay
paintLCD(WRITEOUT, key);
alreadyStored();
}
break;
case SAVE:
savePresetLED();
effectBank = 1;
if (doProg) {
realMode = READ;
alreadyStored();
paintLCD(RUN, 0, 103);
}
if (doSave) {
Serial.println("I am already in save mode waiting for you to pick a slot get off my back");
}
if (key != NOKEY) {
memory(currentBank * bankSize + numberOfPedals * key); // theAddr
paintLCD(SAVED, key);
blinkAnAck(key);
realMode = READ;
paintLCD(RUN, 0, 101);
}
break;
case READ:
clearPresetLED();
if (doBank) {
currentBank++;
if (currentBank > numberOfBanks) currentBank = 1;
Serial.print("preset bank became "); Serial.println(currentBank);
allLampsOff();
paintLCD(RUN, 0, 102);
}
if (doDown) {
currentBank--;
if (currentBank < 1) currentBank = numberOfBanks;
Serial.print("preset bank became "); Serial.println(currentBank);
allLampsOff();
paintLCD(RUN, 0, 102);
}
if (doProg) {
realMode = WRITE;
allLampsOff();
alreadyStored();
paintLCD(PROGRAMMODE);
}
if (doSave) {
paintLCD(DUMBBITCH);
paintLCD(RUN, 0, 104);
}
if (key != NOKEY) {
readPreset(currentBank * bankSize + numberOfPedals * key);
setPresetLED(key, LOW); // all done and only now do we say
paintLCD(READPRESET, key);
}
break;
default:
Serial.print(realMode);
Serial.println(" error");
}
}
void spillMode() {
Serial.println("spillMode broken just now, write it");
}
void setupRelays() { // engages relays on and off at start-up - triggering pin 10 across the board puts them in default bypass mode.
for (int ii = 0; ii < numberOfPedals; ii++) {
pinMode(OneRelayPin[ii], OUTPUT);
pinMode(TenRelayPin[ii], OUTPUT);
digitalWrite(TenRelayPin[ii], rHot); //rHot
okDelay(50);
digitalWrite(TenRelayPin[ii], rGnd); //rGnd
}
// The below is needed only when you have single coil relays
/* for (int ii = 0; ii < numberOfPedals; ii++) {
digitalWrite(OneRelayPin[ii], rHot); //rHot
okDelay(20);
digitalWrite(TenRelayPin[ii], rHot); //rHot
digitalWrite(OneRelayPin[ii], rGnd); //rGnd
okDelay(20);
digitalWrite(TenRelayPin[ii], rGnd); //rGnd
okDelay(20);
}*/
}
void alreadyStored() { // this tells us which preset has something stored during program and save mode
int theBank = currentBank;
for (int thePreset = 0; thePreset < numberOfPedals; thePreset++) {
bool anyEffect = false;
for (int theEffect = 0; theEffect < numberOfPedals; theEffect++) {
unsigned char fxON = EEPROM.read(theBank * bankSize + thePreset * numberOfPedals + theEffect);
if (!fxON) {
anyEffect = true;
break;
}
}
if (anyEffect) {
setPresetLED(thePreset, LOW);
} else {
}
}
}
void setupVLEDs() { // shows the rainbow and sets the neopixels on startup
eLED.begin();
pLED.begin();
sLED.begin();
eLED.show();
pLED.show();
sLED.show();
eLED.setBrightness(40); // up to 255... if you want to go blind, that is. And use a LOT of power.
pLED.setBrightness(40); // IOW, put these below 50. You'll thank yourself.
sLED.setBrightness(40);
rainbow(0); // start the rainbow - has to be zero, otherwise it will never end
eLED.clear();
pLED.clear();
sLED.clear();
eLED.show();
pLED.show();
sLED.show();
}
void rainbow(int wait) { // the rainbow function. duh.
for (long firstPixelHue = 0; firstPixelHue < 5 * 65536; firstPixelHue += 256) {
for (long ii = 0; ii < eLED.numPixels(); ii++) {
long pixelHue = firstPixelHue + (ii * 65536L / eLED.numPixels());
eLED.setPixelColor(ii, eLED.gamma32(eLED.ColorHSV(pixelHue)));
}
for (long ii = 0; ii < pLED.numPixels(); ii++) {
long pixelHue = firstPixelHue + (ii * 65536L / pLED.numPixels());
pLED.setPixelColor(ii, pLED.gamma32(pLED.ColorHSV(pixelHue)));
}
for (long ii = 0; ii < sLED.numPixels(); ii++) {
long pixelHue = firstPixelHue + (ii * 65536L / sLED.numPixels());
sLED.setPixelColor(ii, sLED.gamma32(sLED.ColorHSV(pixelHue)));
}
pLED.show();
eLED.show();
sLED.show();
okDelay(wait);
}
}
char kStoreEffectLED[numberOfPedals];
// set colors for neopixels here
const unsigned long neoClear = 0x000000; // Clear
const unsigned long neoMagenta = 0x800080; // Magenta
const unsigned long neoGreen = 0x00ff00; // Green
const unsigned long neoYellow = 0xffff00; // Yellow
const unsigned long neoOrange = 0xff9900; // Orange Sherbert 66CCCC
const unsigned long neoAqua = 0x66CCCC; // Aqua
void setEffectLED(int theLED, int theValue) { // these are the effect LEDs in read mode
if (theLED <= 7) {
eLED.setPixelColor(theLED, theValue ? 1 : neoYellow);
} else {
eLED.setPixelColor(theLED, theValue ? 1 : neoOrange);
}
eLED.show();
kStoreEffectLED[theLED] = theValue;
}
int getEffectLED(int theLED) { // this allows the neopixels to work in program and save modes
return kStoreEffectLED[theLED] ? 1 : 0;
}
void setPresetLED(int theLED, int theValue) { // these are the preset LEDs in read mode
pLED.setPixelColor(theLED, theValue ? 1 : neoGreen);
pLED.show();
}
void progPresetLED() { // these is the program/save LED color in program mode
for (int ii = 0; ii < numberOfProgNeos; ii++) {
sLED.setPixelColor(ii, neoMagenta);
sLED.show();
}
}
void savePresetLED() { // these is the program/save LED color in save mode
for (int ii = 0; ii < numberOfProgNeos; ii++) {
sLED.setPixelColor(ii, neoAqua);
sLED.show();
}
}
void clearPresetLED() { // these is the program/save LED color in read mode
for (int ii = 0; ii < numberOfProgNeos; ii++) {
sLED.setPixelColor(ii, HIGH);
sLED.show();
}
}
// Blink and it's all over...
static bool blinkingAnAck = false;
static int blinkCounter;
static int blinkingLED;
const byte blinkPeriod = 100; // how long each flash stays on
const byte blinkNTimes = 19; // how many times it blinks - has to be odd to leave the theLED off
void blinkAnAck(int theLED) {
blinkingAnAck = true;
blinkCounter = blinkNTimes;
blinkingLED = theLED;
}
void runAckBlinker() {
if (!blinkingAnAck) return;
static unsigned long lastBlink;
unsigned long now = millis();
if (now - lastBlink > blinkPeriod) {
pLED.setPixelColor(blinkingLED, blinkCounter & 0x1 ? 1 : neoAqua);
pLED.show();
blinkCounter--;
lastBlink = now;
}
if (!blinkCounter) killAckBlinker();
}
void runAckBlinker(bool killMe) {
if (killMe) killAckBlinker();
else runAckBlinker();
}
void killAckBlinker() {
if (blinkingAnAck) {
eLED.clear();
blinkingAnAck = false;
}
}
void beginLCD() {
lcd.begin(16, 2);
// custom LCD characters - used only during write/program mode
byte effChosenLCD[8] = { 0b11111, 0b10101, 0b11111, 0b10101, 0b11111, 0b11111, 0b11011, 0b11111 }; // pedal icon - also dominos
byte effBankLCD[8] = { 0b00000, 0b00000, 0b11111, 0b01110, 0b00100, 0b00000, 0b00000, 0b00000 }; // down arrow
lcd.createChar(0, effChosenLCD);
lcd.createChar(1, effBankLCD);
}
unsigned long lcdDwellTime; // timer for transient screen messages
const unsigned long DWELL = 2500; // holds off a pending escreen for that long - long so we can see it
// get around to displaying this after the transient message blocked it from being
byte pendedScreenName = NOSCREEN;
byte pendedArgument;
byte pendedExtra;
void serviceLCD(bool userInput) {
if ((lcdDwellTime && now - lcdDwellTime > DWELL) || userInput) {
lcd.clear();
lcdDwellTime = 0; // means LCD is free for all
if (pendedScreenName != NOSCREEN)
paintLCD(pendedScreenName, pendedArgument, pendedExtra);
pendedScreenName = NOSCREEN; // because we out of this world of pain
}
}
void paintLCD(byte screenName) {
paintLCD(screenName, 97, 98);
}
void paintLCD(byte screenName, byte argument) {
paintLCD(screenName, argument, 99);
}
void paintLCD(byte screenName, byte argument, byte extra) {
if (lcdDwellTime) {
pendedScreenName = screenName;
pendedArgument = argument;
pendedExtra = extra;
return;
}
switch (screenName) {
case DUMBBITCH:
lcdDwellTime = now; // transient message. time or user activity moves to the pended screen
dumbBitchLCD();
break;
case READPRESET:
readPresetLCD(argument);
break;
case WRITEOUT:
writeOutLCD(argument);
break;
case RUN:
runLCD(extra);
break;
case SAVED:
lcdDwellTime = now;
savedLCD(argument);
break;
case PROGRAMMODE:
programModeLCD();
break;
case SAVEMODE:
saveModeLCD();
break;
case SPLASH:
lcdDwellTime = now;
splashLCD();
break;
default:
Serial.print(screenName);
Serial.println(" error unknown screen name/number");
}
}
// LCD readouts for 16x2 LCD display - instead of using lcd.clear each time, just put in spaces until you get to 16 spaces per line.
void dumbBitchLCD() {
lcd.setCursor(0, 0);
lcd.print("Nope... ");
lcd.setCursor(0, 1);
lcd.print("Dumb Bitch! ");
}
void readPresetLCD(byte theLED) {
lcd.setCursor(0, 0);
lcd.print(" B-");
lcd.print(currentBank);
lcd.setCursor(0, 1);
lcd.print("Preset ");
lcd.print(theLED + 1);
lcd.print(" ");
}
void runLCD(byte wtf) {
Serial.print(wtf); Serial.println(" paint run mode LCD");
lcd.setCursor(0, 0);
lcd.print(" B-");
lcd.print(currentBank);
lcd.setCursor(0, 1);
lcd.print("Press Any Preset");
}
// writeOutLCD displays the effects from right to left, which is how they would be chained together in the real world
void writeOutLCD(byte) {
lcd.setCursor(0, 0);
if (effectBank == 1) { // byte 1 is down arrows denoting active effectBank,
lcd.print(" FX-2 ");
lcd.write((byte)1);
lcd.write((byte)1);
lcd.print("FX-1");
lcd.write((byte)1);
lcd.write((byte)1);
} else if (effectBank == 2) {
lcd.write((byte)1);
lcd.write((byte)1);
lcd.print("FX-2");
lcd.write((byte)1);
lcd.write((byte)1);
lcd.print(" FX-1 ");
}
for (int ii = 0; ii < numberOfPedals; ii++) { // byte 0 is the pedal/domino shape
if (getEffectLED(ii) == LOW) {
lcd.setCursor(((numberOfPedals - 1) - ii), 1); lcd.write((byte)0);
} else {
lcd.setCursor(((numberOfPedals - 1) - ii), 1); lcd.print(" ");
}
}
}
void savedLCD(byte theLED) {
lcd.setCursor(0, 0);
lcd.print("Program saved to");
lcd.setCursor(0, 1);
lcd.print("Bank ");
lcd.print(currentBank);
lcd.print(" Preset ");
lcd.print(theLED + 1);
lcd.print(" ");
}
void programModeLCD() {
lcd.setCursor(0, 0);
lcd.print("Program Mode B-");
lcd.print(currentBank);
lcd.setCursor(0, 1);
lcd.print("Select Pedals ");
}
void saveModeLCD() {
lcd.setCursor(0, 0);
lcd.print("Save Mode ");
lcd.setCursor(0, 1);
lcd.print("Select Preset ");
}
void splashLCD() { // a lovely affirmation to set off each session right!
lcd.setCursor(0, 0);
lcd.print("You Gorgeous Fat");
lcd.setCursor(0, 1);
lcd.print("Sausage Bitch! ");
}
// Tryna' set me up? Better be ready to finish the job...
void setup() {
Serial.begin(1000000); /* not for midi communication - pin 1 TX */
Serial.println("first things first.\n");
beginLCD();
paintLCD(SPLASH);
for (int ii = 0; ii < numberOfPhotos; ii++) {
pinMode(photoPin[ii], OUTPUT);
digitalWrite(photoPin[ii], rHot);
}
setupVLEDs(); // see if they're working - wow, rainbow!
setupRelays(); // relay pins and initial state
ezBankButton.setDebounceTime(10);
ezProgButton.setDebounceTime(10);
ezSaveButton.setDebounceTime(10);
ezDownButton.setDebounceTime(10);
for (int ii = 0; ii < numberOfPhotos; ii++) {
digitalWrite(photoPin[ii], rGnd);
}
paintLCD(RUN, 0, 111);
allLampsOff();
}UP
<----- PRESET 1
PRESET 8 <-----
<----- EFFECT 1
EFFECT 16 <-----
TUNER
TUNER RELAY
PROG/SAVE
PHOTO
DOWN
PROG
SAVE