/*
Simulation of a DMX-recorder/player menu,
but made with text debugging, cause of the lack of DMX.
This project is made on an Arduino Mega that uses the Arduino Ethernet Shield 2 and the CTC-DRA-10-R2 DMX module.
Because the UNO version doesn't have enough RAM and Flash
when DMX and SD are included.
Beware, the Atmega2560 isn't fast/powerful enough which causes it to skip around 15 packets
(1 packet == the values of the 512 channels).
Because of this, the recorded scene or show won't be as smooth as the original.
However, this can be fixed by limiting the number of channels that it needs to record data from,
but this requires a delay of 50 milliseconds to be added after each packet when playing.
The DMX data files are stored as .txt on a SD-card, when the Atmega2560 is resetted the SD will still have these files,
thus is can still play this DMX data.
A visual block diagram of the menu structure can be seen with the following link:
https://www.figma.com/board/HslhSNgoAhSBVZULb9AF7i/Blokschema-DMX-Menu-V3?node-id=0-1&t=vpAaETJo9xo2n8Al-1
To navigate through the menu:
U can use your arrow keys on your keyboard or the gray buttons.
For the function of playing a recording using an input,
u can use the keys 1 or 2.
To get the effect of
Digital 01 → press button D1 or key 1
Digital 10 → press button D2 or key 2
Digital 11 → press button D1 and D2 at the same time (in simulation, not possible)
or key 1 and key 2 at the same time.
*/
#include <Arduino.h>
#include <Wire.h>
#include <SSD1306Ascii.h> //This library is used because it uses less memory, but has less smooth transitions.
#include <SSD1306AsciiWire.h>
#define I2C_ADDRESS 0x3C // Change this to your display's I2C address
SSD1306AsciiWire oled; // Create an instance of the SSD1306Ascii class
// Define button pins
#define BUTTON_ENTER 2 // Enter button
#define BUTTON_BACK 3 // Back button
#define BUTTON_UP 5 // Up button
#define BUTTON_DOWN 6 // Down button
const int INPUT_EX_1 = 8; // Pin for external button 1
const int INPUT_EX_2 = 9; // Pin for external button 2
// Menu states
// To make sure the code knows what part of the menu it's located.
// Every state and its items are shown in the block diagram on Figma.
enum State
{
HOME,
PLAYER_MENU,
PLAYER_OPTIONS,
PLAY_INPUT,
RECORDER_MENU,
RECORDING_DATA,
SAVE_RECORDING,
CONFIRM_SAVE,
CONFIRM_DELETE
};
// Menu items
// Menu items to be printed for each for the menu states
// For some menu states the same items are used, because of that there is only need for 1 array of items.
const char *homeMenu[] = {"Player", "Recorder"};
const char *confirm[] = {"Yes/Confirm", "No/Cancel"};
const char *recordings[] = {"Recording 1", "Recording 2", "Recording 3", "Recording 4", "Recording 5"};
const char *playerOptions[] = {"Play Loop", "Play Input", "Stop Loop"};
const char *selectFastInput[] = {"Digital 01", "Digital 10", "Digital 11"};
const char *recorderMenu[] = {"Start Recording", "Save Recording", "Restore Default"};
const char *recordingData[] = {"Stop"};
// Variables
State currentState = HOME; // Initial state
State previousState = HOME; // Previous state
int menuIndex = 0; // Track selected item
int topVisibleIndex = 0; // Track the top visible item for scrolling
int currentRecording = 0; // What recording is the user navigating in
int recordingInput01; // Recording that belongs to Input01
int recordingInput10; // Recording that belongs to Input10
int recordingInput11; // Recording that belongs to Input11
int recordingLoop = 0; // Recording that is playing in Loop
unsigned long lastButtonPressTime = 0; // Debounce Menu buttons
const unsigned long debounceDelay = 200; // Debounce Menu buttons
unsigned long previousMillisInput = 0; // Debounce Input buttons
const unsigned long delayLoop = 1000; // Every 1 second there will be a printout to simulate a DMX-recording in loop
unsigned long previousMillisLoop = 0; // Last time a loop was played
boolean stateRecorderPlayer = false; // false = player state; true = recorder state
boolean playLoop = false; // If true than there will be a recording playing in loop.
boolean recording = false; // If true the there is DMX data being recorded
boolean recorded = false; // There is a temporary recording saved
boolean readInput = false; // When a digital input is assigned it will also check if there is a signal to play the wanted recording.
// Test
String stringRecording1; // Testdata
String stringRecording2;
String stringRecording3;
String stringRecording4;
String stringRecording5;
boolean recording1 = false; // Testdata to know if a recording is made (for the actual recorder there will be a text file)
boolean recording2 = false;
boolean recording3 = false;
boolean recording4 = false;
boolean recording5 = false;
// Methods
void handleMenu(); // What happens when one of the menu buttons is pressed.
void updateDisplay(); // Every time a menu button is pressed,
// the screen will be refreshed to show the current menu
// and items or the new selected items.
void drawMenuItems(const char *menuItems[], int itemCount); // The menu items will be drawn and the current item will be shown be a > .
void updateCurrentRecording(); // When a recording needs to be choosen in player or recorder mode.
void deleteAll(); // To bring the recorder to its default state, all recording will be deleted
// and the booleans will become false.
void digitalInput(); // When the corresponding boolean is true,
// the recorder will check when the chosen button or
// combination is activated to play the chosen recording.
void playRecordingLoop(); // When the corresponding boolean is true,
// the chosen DMX-recording will be played in loop.
void startStopRecording(); // This starts or stops a recording of DMX data.
void saveRecording(); // This sets the boolean of the choosen recording slot true.
boolean validRecording(int nrRecording); // Checks if the boolean of the corresponding recording is true.
int getItemCount(); // How many items are there for each state
void setup()
{
Serial.begin(9600);
delay(100); // time to power on
Wire.begin(); // Start I2C
oled.begin(&Adafruit128x64, I2C_ADDRESS); // Initialize the display
oled.setFont(System5x7); // Set a default font
oled.clear(); // Clear the display
pinMode(BUTTON_ENTER, INPUT);
pinMode(BUTTON_BACK, INPUT);
pinMode(BUTTON_UP, INPUT);
pinMode(BUTTON_DOWN, INPUT);
pinMode(INPUT_EX_1, INPUT); // Set button 1 pin as input with internal pull-up
pinMode(INPUT_EX_2, INPUT); // Set button 2 pin as input with internal pull-up
updateDisplay();
pinMode(LED_BUILTIN, OUTPUT);
}
void loop()
{
handleMenu();
digitalInput();
playRecordingLoop();
}
void handleMenu()
{
unsigned long currentTime = millis();
// Menu item
if (!digitalRead(BUTTON_UP) && (currentTime - lastButtonPressTime > debounceDelay))
{
lastButtonPressTime = currentTime;
while (!digitalRead(BUTTON_UP))
; // To stop repeated call when pressed. When the button is released only then the code will proceed.
if (menuIndex > 0)
menuIndex--; // Navigate to menu item below
if (menuIndex < topVisibleIndex) // This was used when the menu had more items than there could fit at once on the display.
topVisibleIndex = menuIndex;
updateDisplay();
}
if (!digitalRead(BUTTON_DOWN) && (currentTime - lastButtonPressTime > debounceDelay))
{
lastButtonPressTime = currentTime;
while (!digitalRead(BUTTON_DOWN))
;
int itemCount = getItemCount();
if (menuIndex < itemCount - 1)
menuIndex++; // Navigate to menu item above
if (menuIndex >= topVisibleIndex + 5)
topVisibleIndex = menuIndex - 3;
updateDisplay();
}
// Menu state -----------------------------------------------------------------------------------------------
if (!digitalRead(BUTTON_ENTER) && (currentTime - lastButtonPressTime > debounceDelay))
{
lastButtonPressTime = currentTime;
while (!digitalRead(BUTTON_ENTER))
;
previousState = currentState; // We save the previous state before changing the variable of the current one.
switch (previousState) // We check what state the menu is, by looking at the previousstate variable, and then check what item is selected.
{
case HOME:
if (menuIndex == 0) // Player item
{
currentState = PLAYER_MENU; // The current state changes to what was choosen by the user.
stateRecorderPlayer = false;
}
else if (menuIndex == 1) // Recorder item
{
currentState = RECORDER_MENU;
stateRecorderPlayer = true;
}
break;
// Player ----------------------------------------------------------------------------------------
case PLAYER_MENU:
currentState = PLAYER_OPTIONS;
updateCurrentRecording(); // Choosing recording
break;
case PLAYER_OPTIONS:
if (menuIndex == 0) // Start Loop Item
{
playLoop = true; // The choosen recording will be played in loop.
readInput = false; // This disabled the functionality of the input buttons.
recordingLoop = currentRecording; // The recoding which is to played in loop is the recording that was selected before this.
currentState = PLAYER_OPTIONS;
}
else if (menuIndex == 1) // Choose input item
{
currentState = PLAY_INPUT;
}
else if (menuIndex == 2) // Stop Loop Item
{
if (playLoop)
{ // If there was a loop active, it will be stopped and the file will be closed.
playLoop = false;
if (recordingInput01 > 0 || recordingInput10 > 0 || recordingInput11 > 0)
{ // If there was a input choosen previously it will be activated again.
readInput = true;
}
Serial.print("Stop play: Recording ");
Serial.println(recordingLoop);
}
currentState = PLAYER_OPTIONS;
}
break;
case PLAY_INPUT: // choosing the digital input
currentState = PLAY_INPUT;
if (menuIndex == 0 && validRecording(currentRecording)) // Input01 item
{
readInput = true;
recordingInput01 = currentRecording;
Serial.print("Digital input 01 Recording nr ");
Serial.println(currentRecording);
}
else if (menuIndex == 1 && validRecording(currentRecording)) // Input10 item
{
readInput = true;
recordingInput10 = currentRecording;
Serial.print("Digital input 10 Recording nr ");
Serial.println(currentRecording);
}
else if (menuIndex == 2 && validRecording(currentRecording)) // Input11 item
{
readInput = true;
recordingInput11 = currentRecording;
Serial.print("Digital input 11 Recording nr ");
Serial.println(currentRecording);
}
break;
// Recorder -------------------------------------------------------------------------------------------
case RECORDER_MENU:
if (menuIndex == 0) // start recording item
{
currentState = RECORDING_DATA;
startStopRecording();
recorded = false; // To make sure that there can't be a recording saved when there might not be one.
}
else if (menuIndex == 1) // select recording item
{
if (recorded)
{
currentState = SAVE_RECORDING;
}
else
{
currentState = RECORDER_MENU;
}
}
else if (menuIndex == 2) // "Restore to default/delete all" item
{
currentState = CONFIRM_DELETE;
}
break;
case RECORDING_DATA: // To stop recording DMX data
startStopRecording();
currentState = RECORDER_MENU;
break;
case SAVE_RECORDING: // To save the recorded DMX data into a recording slot
currentState = CONFIRM_SAVE;
updateCurrentRecording();
break;
case CONFIRM_SAVE: // To select the recorded DMX data into a recording slot
if (menuIndex == 0) // Yes/Confirm item --> save
{
currentState = RECORDER_MENU;
Serial.print("Confirm Save Recording ");
Serial.println(currentRecording);
saveRecording();
recorded = false;
}
else if (menuIndex == 1) // No/Cancel item --> don't save
{
currentState = SAVE_RECORDING;
}
break;
case CONFIRM_DELETE:
if (menuIndex == 0) // Yes/Confirm item --> delete all
{
currentState = RECORDER_MENU;
Serial.println("Confirmed Delete All");
deleteAll();
}
else if (menuIndex == 1) // No/Cancel delete all --> don't delete all
{
currentState = RECORDER_MENU;
}
break;
default:
break;
}
menuIndex = 0;
updateDisplay();
}
if (!digitalRead(BUTTON_BACK) && (currentTime - lastButtonPressTime > debounceDelay))
{
lastButtonPressTime = currentTime;
while (!digitalRead(BUTTON_BACK))
;
switch (currentState)
{
// To make sure the user is able to return to all the states before the current one.
// This is necasary because the previous menu state isn't always the menu state above the current one.
// For example when delete all is confirmed, the user goes back to the RECORDER_MENU,
// if back is now pressed the user will go back to the confirmation instead of the Home page.
case HOME:
previousState = HOME;
break;
// player
case PLAYER_MENU:
previousState = HOME;
break;
case PLAYER_OPTIONS:
previousState = PLAYER_MENU;
break;
case PLAY_INPUT:
previousState = PLAYER_OPTIONS;
break;
// Recorder
case RECORDER_MENU:
previousState = HOME;
stateRecorderPlayer = false;
break;
case RECORDING_DATA: // Recording Cancelled
recording = false;
recorded = false;
Serial.println("Recording Cancelled");
break;
case SAVE_RECORDING:
previousState = RECORDER_MENU;
break;
case CONFIRM_SAVE:
previousState = SAVE_RECORDING;
break;
case CONFIRM_DELETE:
previousState = RECORDER_MENU;
break;
default:
break;
}
currentState = previousState;
menuIndex = 0;
updateDisplay();
}
}
void updateDisplay()
{
oled.clear(); // Clear the display
oled.setCursor(0, 0); // Set cursor position
switch (currentState) // Looks at the current menu state to decide what title to use and what items to draw
{
case HOME:
oled.println(F("Home Page")); // Title if the state shown on the display
drawMenuItems(homeMenu, 3);
break;
case PLAYER_MENU:
oled.println(F("Player Menu"));
drawMenuItems(recordings, 5);
break;
case PLAYER_OPTIONS:
oled.println(F("Player Options "));
oled.print(F("Recording "));
oled.println(currentRecording);
drawMenuItems(playerOptions, 3);
break;
case PLAY_INPUT:
oled.println(F("Select Digital "));
oled.println(F("input"));
drawMenuItems(selectFastInput, 3);
break;
case RECORDER_MENU:
oled.println(F("Recorder Menu"));
drawMenuItems(recorderMenu, 3);
break;
case RECORDING_DATA:
oled.println(F("Recording Data"));
drawMenuItems(recordingData, 1);
break;
case SAVE_RECORDING:
oled.println(F("Save to"));
drawMenuItems(recordings, 5);
break;
case CONFIRM_SAVE:
oled.print(F("Save recording "));
oled.println(currentRecording);
drawMenuItems(confirm, 2);
break;
case CONFIRM_DELETE:
oled.println(F("Delete all"));
drawMenuItems(confirm, 2);
break;
}
}
void drawMenuItems(const char *menuItems[], int itemCount)
{
// The visible index can be used to show a limited number of items,
// but when this number is exceeded it will show the next item on the top
int endIndex = min(topVisibleIndex + 5, itemCount);
for (int i = topVisibleIndex; i < endIndex; i++)
{
// Drawing the menu items with the current selected item shown by a "> ".
if (i == menuIndex)
oled.print("> "); // Selected item
else
oled.print(" ");
oled.println(menuItems[i]);
}
}
// Update the current recording number
void updateCurrentRecording()
{
currentRecording = (menuIndex < 5) ? menuIndex + 1 : 0; // Limit to the first five recordings
}
int getItemCount()
{ // How many items for each menu state
switch (currentState)
{
case HOME:
return 2;
case PLAYER_MENU:
return 5;
case PLAYER_OPTIONS:
return 3;
case PLAY_INPUT:
return 3;
case RECORDER_MENU:
return 3;
case RECORDING_DATA:
return 1;
break;
case SAVE_RECORDING:
return 5;
case CONFIRM_SAVE:
return 2;
case CONFIRM_DELETE:
return 2;
default:
return 0;
}
}
void digitalInput()
{
if (readInput && !stateRecorderPlayer)
{
/* Reading external digital inputs */
unsigned long currentMillis = millis();
// Only check button states at set intervals
if (currentMillis - previousMillisInput >= debounceDelay)
{
previousMillisInput = currentMillis;
// Check for button press states and print accordingly
if (!digitalRead(INPUT_EX_1) && !digitalRead(INPUT_EX_2)) // When INPUT_EX_1 and INPUT_EX_2 are pressed at the same time --> Input11
{
while (!digitalRead(INPUT_EX_1) && !digitalRead(INPUT_EX_2))
;
Serial.println("Input 11");
if (recordingInput11 != 0) // Recording = 0 means there is no recording signed to the input
{
Serial.print("Recording ");
Serial.println(recordingInput11);
digitalWrite(LED_BUILTIN, HIGH);
delay(2000);
digitalWrite(LED_BUILTIN, LOW);
delay(2000);
digitalWrite(LED_BUILTIN, HIGH);
delay(2000);
digitalWrite(LED_BUILTIN, LOW);
}
}
else if (!digitalRead(INPUT_EX_1)) // When INPUT_EX_1 is pressed --> Input01
{
while (!digitalRead(INPUT_EX_1))
;
Serial.println("Input 01");
if (recordingInput01 != 0)
{
Serial.print("Recording ");
Serial.println(recordingInput01);
digitalWrite(LED_BUILTIN, HIGH);
delay(500);
digitalWrite(LED_BUILTIN, LOW);
delay(500);
digitalWrite(LED_BUILTIN, HIGH);
delay(500);
digitalWrite(LED_BUILTIN, LOW);
}
}
else if (!digitalRead(INPUT_EX_2)) // When INPUT_EX_2 is pressed --> Input10
{
while (!digitalRead(INPUT_EX_2))
;
Serial.println("Input 10");
if (recordingInput10 != 0)
{
Serial.print("Recording ");
Serial.println(recordingInput10);
digitalWrite(LED_BUILTIN, HIGH);
delay(1000);
digitalWrite(LED_BUILTIN, LOW);
}
}
}
}
}
void playRecordingLoop()
{
if (playLoop && recordingLoop != 0 && validRecording(recordingLoop) && !stateRecorderPlayer)
{
unsigned long currentMillis = millis();
if (currentMillis - previousMillisLoop >= delayLoop)
{
previousMillisLoop = currentMillis;
Serial.print("Playing in loop: recording ");
Serial.println(recordingLoop);
if (digitalRead(LED_BUILTIN) == LOW)
{
digitalWrite(LED_BUILTIN, HIGH);
}
else
{
digitalWrite(LED_BUILTIN, LOW);
}
}
}
else
{
recordingLoop = 0;
}
}
boolean validRecording(int nrRecording)
{
switch (nrRecording)
{
case 1:
return recording1;
break;
case 2:
return recording2;
break;
case 3:
return recording3;
break;
case 4:
return recording4;
break;
case 5:
return recording5;
break;
default:
break;
}
}
void startStopRecording()
{
if (!recording)
{ // If there is already data being recorded
recording = true;
recorded = false;
Serial.println("Start Recording");
}
else
{
recording = false;
recorded = true;
Serial.println("Stop Recording");
}
}
void saveRecording()
{
switch (currentRecording)
{
case 1:
recording1 = true;
break;
case 2:
recording2 = true;
break;
case 3:
recording3 = true;
break;
case 4:
recording4 = true;
break;
case 5:
recording5 = true;
break;
default:
break;
}
Serial.print("Recording ");
Serial.print(currentRecording);
Serial.println(" Saved");
}
void deleteAll() // Remove all recordings from SD card and set all booleans to default
{
if (recording1)
{
stringRecording1 = "";
recording1 = false;
}
if (recording2)
{
stringRecording2 = "";
recording2 = false;
}
if (recording3)
{
stringRecording3 = "";
recording3 = false;
}
if (recording4)
{
stringRecording4 = "";
recording4 = false;
}
if (recording5)
{
stringRecording5 = "";
recording5 = false;
}
stateRecorderPlayer = false;
recording1 = false;
recording2 = false;
recording3 = false;
recording4 = false;
recording5 = false;
playLoop = false;
recording = false;
recorded = false;
readInput = false;
recordingInput01 = 0;
recordingInput10 = 0;
recordingInput11 = 0;
recordingLoop = 0;
}Loading
ssd1306
ssd1306