Introduction: DMX Effect Controller
The goal of this project was to make a DMX-Controlled plug box which would allow on-stage effects or devices to be triggered remotely from the lighting console. This could potentially eliminate the need for a separate control system and operator backstage. This controller could be used in a wide variety of applications including:
-Triggering FX (solenoid, motor, other micro-controller, etc.)
-Triggering practical props (lamps, radios, buzzers, etc.)
-Backstage cue light systems
The controller includes standard 5-pin DMX input and thru connectors mounted to the exterior of the box, which allows it to easily be daisy chained with other DMX peripherals. There is an onboard channel selector knob and display which allow the user to select and save the first of the four controllable DMX addresses/channels
The receptacles in this controller are not dimmable; they are meant only to provide 120V AC power on cue. Each receptacle has a maximum load of 10A due to the limits of the relays and wiring.
Step 1: What You'll Need
Physical Components:
- Enclosure box for:
- 2 dual 120V/15A receptacles
- All Electronic components
- Knockouts for AC power, DMX Input, DMX Thru
- Cutouts for LCD display, Rotary encoder knob
- AC power cord
- 16ga. wire for wiring relays to receptacles
- Jumper wires for electronics
- Assorted mounting screws and posts
- Arduino UNO and USB cable
- Rotary encoder w/button capability
- Nifty 3D printed knob (or buy one or don't)
- 4 digit, 7 segment LCD display
- MAX485 RS-485 module
- Panel mount DMX (male and female)
- Power supply (I used 12v)
- 5V 4 relay module
Arduino Libraries Used:
- EEPROM.h (to save DMX address in onboard memory)
- Arduino.h
- TM1637Display.h (LCD Display)
- Conceptinetics.h (DMX Slave)
Step 2: Suggestion: Breadboard Prototyping
I would highly recommend breadboarding, assembling all of the electronic components, uploading the sketch, connecting to a DMX controller (like a lighting control console) and testing things out before adding the AC power components and the enclosure. This way, you'll know you're on the right track before you have everything crammed in the box and it becomes nearly impossible to make quick alterations.
Step 3: MAX485 DMX Slave Setup
I used a MAX485 module set up as a "slave" to receive the DMX packets. The connections are as follows:
- GND to 5-pin DMX pin1, to DE &RE, and to GND on Arduino
- A to 5-pin DMX pin3
- B to 5-pin DMX pin2
- VCC to +5V
- RO to Pin0 on Arduino
- DI not usedpin
-Later in the process you will use jumpers to connect pins 1-3 of the DMX input jack to pins 1-3 of the DMX thru jack.
-I later used the far end of this PCB to consolidate all ground and +5v connections before jumping one wire to the Arduino
*IMPORTANT* - You must unplug the jumper to Pin0 any time you upload a sketch, as this pin is also used in serial com. when uploading.
Step 4: 4 Digit 7 Segment LCD Display
This display is used to read out the DMX start address (out of 4) that has been selected with the encoder.
I cut a hole in the enclosure and created custom mounts for this piece of hardware. Connections are as follows:
GND -> GND
VCC -> +5V
DIO -> Pin5 (orange wire)
CLK -> Pin6 (yellow wire
Step 5: Encoder Knob and Button
I used a 20 position rotary encoder w/push button functionality so that the knob could be used to set the DMX start address and, with a button push, save it to EEPROM so that the unit can be unplugged from power without forgetting a saved address. The address CAN be actively changed with the rotary encoder even after the address is saved, but the saved address will re-load on reset. With a slight tweak in the code, you can make it so the saved address cannot be overwritten by turning the knob without a re-set.
Here are the connections:
GND -> Arduino GND
VCC -> +5V
CLK -> Pin2 (purple)
DT -> Pin3 (gray)
SW (button) -> Pin4 (white)
The encoder is mounted next to the display and was mounted by drilling a hole and using then included nut and washer.
I decided to 3D print a custom knob for my controller box using Fusion 360 and a MakerBot Replicator.
Attachments
Step 6: Enclosure and Mounting
After you've tested all of the previous steps to ensure the functionality of the electronics and sketch, it's time to make a permanent enclosure.
I used a 6x8 junction box from Amazon (Bud Industries brand) and modified it as needed. As you'll see later, things fit pretty snugly in the end.
I also created a mounting tray out of some spare acrylic to keep the main electronic components in place and stable. This allowed many of the components to be accessed easily for assembly before dropping them into the box later on
Step 7: Relays
I used a 4 channel relay module to control each receptacle separately. The AC hot comes into the COM terminal on the first relay and then jumps to the COM on each successive relay. From the NO (normally open) screw terminal of each relay, you'll wire to the hot terminal of the corresponding receptacle. When the relays receive signal from the Arduino, they will close as prompted and supply power to the correct receptacle
Connections:
Ch 1 - Pin8 (blue)
Ch 2 - Pin9 (purple)
Ch 3 - Pin10 (gray)
Ch 4 - Pin11 (white)
Step 8: AC Wiring
*NOTE*: Please to not attempt this step without adequate knowledge of AC electricity. Any mistep could result in injury or electrocution!
- Be sure to remove any connecting tabs between the top and bottom receptacle on each gang of two so that you have individual control of each receptacle
- Wire the leads from the relays to the hot side of each corresponding receptacle (Chs. 1-4)
- Wire the initial hot AC lead to the incoming AC power cord
- Jumper the neutral wires in order from Ch1 -> Ch4 and then wire from the last receptacle neutral to the AC power cord neutral.
- Tie in jumpers to power the power supply to the Arduino. I used wire nuts for these connections, but screw terminals would have been nice if they were available and had fit in the enclosure.
Step 9: Make All Final Connections
Before putting the lid on, make sure all connections have been made, for AC power, DMX jacks, and all Arduino/electronics.
If you haven't already uploaded the sketch, unplug the jumper from Pin0 on the Arduino and upload, then replace the Pin0 jumper.
Step 10: Close It Up
If all has gone well, it should look a little something like this. Normally the display will read out the DMX start channel. When you push the button to save the channel, the word "done" will appear for one second to confirm the save.
Step 11: Make Things Happen!
Simply connect to a lighting console via the DMX input jack, set your starting address on box, make sure things are addressed correctly in the console, and then start turning things on and off.
REMEMBER: Each receptacle has a max load of 10A. DO NOT EXCEED THIS!
ALSO: Upon initial powering up of the box, all four relays will briefly flash on. I haven't figured out how to fix this yet, but for now you just have to make sure that nothing is plugged into the box on startup that you don't want to activate momentarily. Best practice: Power up the box, THEN plug devices into receptacles
Step 12: The Code
#include <EEPROM.h>
#include <Arduino.h> #include <TM1637Display.h> #include <Conceptinetics.h>
// Rotary encoder declarations static int pinA = 2; // Our first hardware interrupt pin is digital pin 2 static int pinB = 3; // Our second hardware interrupt pin is digital pin 3 volatile byte aFlag = 0; // let's us know when we're expecting a rising edge on pinA to signal that the encoder has arrived at a detent volatile byte bFlag = 0; // let's us know when we're expecting a rising edge on pinB to signal that the encoder has arrived at a detent (opposite direction to when aFlag is set) volatile int encoderPos = 0; //this variable stores our current value of encoder position. Change to int or uin16_t instead of byte if you want to record a larger range than 0-255 volatile int oldEncPos = 0; //stores the last encoder position value so we can compare to the current reading and see if it has changed (so we know when to print to the serial monitor) volatile byte reading = 0; //somewhere to store the direct values we read from our interrupt pins before checking to see if we have moved a whole detent
// Button reading, including debounce without delay function declarations const byte buttonPin = 4; // this is the Arduino pin we are connecting the push button to byte oldButtonState = HIGH; // assume switch open because of pull-up resistor const unsigned long debounceTime = 10; // milliseconds unsigned long buttonPressTime; // when the switch last changed state boolean buttonPressed = 0; // a flag variable
// Display Module connection pins (Digital Pins) #define CLK 6 #define DIO 5
const uint8_t SEG_DONE[] = { SEG_B | SEG_C | SEG_D | SEG_E | SEG_G, // d SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F, // O SEG_C | SEG_E | SEG_G, // n SEG_A | SEG_D | SEG_E | SEG_F | SEG_G // E };
TM1637Display display(CLK, DIO);
//set the number of DMX channels to be controlled #define DMX_SLAVE_CHANNELS 4
DMX_Slave dmx_slave ( DMX_SLAVE_CHANNELS ); // Configure a DMX slave controller
//define pins to control relays const int CH1 = 8; const int CH2 = 9; const int CH3 = 10; const int CH4 = 11;
//set up EEPROM int address = 1; //DMX starting address int addr = 0; //EEPROM address int a = 0; //holds value for EEPROM data division int b = 0; //holds value for EEPROM data division int val; //stores value of dmx int mapVal; //stores mapped value of dmx
void setup() {
//Rotary encoder section of setup pinMode(pinA, INPUT_PULLUP); // set pinA as an input, pulled HIGH to the logic voltage (5V or 3.3V for most cases) pinMode(pinB, INPUT_PULLUP); // set pinB as an input, pulled HIGH to the logic voltage (5V or 3.3V for most cases) attachInterrupt(0, PinA, RISING); // set an interrupt on PinA, looking for a rising edge signal and executing the "PinA" Interrupt Service Routine (below) attachInterrupt(1, PinB, RISING); // set an interrupt on PinB, looking for a rising edge signal and executing the "PinB" Interrupt Service Routine (below)
// button section of setup pinMode (buttonPin, INPUT_PULLUP); // setup the button pin // Button reading with non-delay() debounce - thank you Nick Gammon! byte buttonState = digitalRead (buttonPin); if (buttonState != oldButtonState) { if (millis () - buttonPressTime >= debounceTime) { // debounce buttonPressTime = millis (); // when we closed the switch oldButtonState = buttonState; // remember for next time if (buttonState == LOW) { //this means the button has been pressed buttonPressed = 1; } else { //this means the button is not currently pressed buttonPressed = 0; } } // end if debounce time up } // end of state change
// Enable DMX slave interface and start recording // DMX data dmx_slave.enable ();
// Set relay pins as outputs pinMode (CH1, OUTPUT ); pinMode (CH2, OUTPUT ); pinMode (CH3, OUTPUT ); pinMode (CH4, OUTPUT );
// Load saved DMX address int a = EEPROM.read(0); int b = EEPROM.read(1); address = a * 256 + b; encoderPos = (address); //set encoder value to start at saved address dmx_slave.setStartAddress (encoderPos); }
void loop() { rotaryMenu();
int k; uint8_t data[] = { 0xff, 0xff, 0xff, 0xff }; display.setBrightness(0x0f);
// Show decimal numbers with/without leading zeros bool lz = false; for (uint8_t z = 0; z < 2; z++) { for (k = 0; k < 10000; k += k * 4 + 7) { display.showNumberDec(encoderPos); } lz = true; } //adjust DMX start address with encoder change dmx_slave.setStartAddress (encoderPos); //save DMX address to EEPROM and display "done" when set if (digitalRead (buttonPin) == LOW) { a = encoderPos / 256; //split DMX address into two parts so that it can fit in the EEPROM b = encoderPos % 256; EEPROM.write(0, a); //store dmx address in EEPROM EEPROM.write(1, b); // Done! display.setSegments(SEG_DONE); delay (1000);
} { // If the channel comes above 10% the relay will switch on // and below 10% the relay will be turned off //REMEMBER: Relays are on when "LOW" and off when "HIGH"
// NOTE: // getChannelValue is relative to the configured start address if ( dmx_slave.getChannelValue (1) > 25 ) digitalWrite ( CH1, LOW ); else digitalWrite ( CH1, HIGH ); if ( dmx_slave.getChannelValue (2) > 25 ) digitalWrite ( CH2, LOW ); else digitalWrite ( CH2, HIGH ); if ( dmx_slave.getChannelValue (3) > 25 ) digitalWrite ( CH3, LOW ); else digitalWrite ( CH3, HIGH ); if ( dmx_slave.getChannelValue (4) > 25 ) digitalWrite ( CH4, LOW ); else digitalWrite ( CH4, HIGH ); }
}
void rotaryMenu() { //This handles the bulk of the menu functions without needing to install/include/compile a menu library //Rotary encoder update display if turned if (oldEncPos != encoderPos) { // DEBUGGING oldEncPos = encoderPos;// DEBUGGING
delay(10); } //Do not allow encoder to read below 1 or above 508 if (encoderPos < 1) { encoderPos = 1; } if (encoderPos > 508) { encoderPos = 508; }
}
//Rotary encoder interrupt service routine for one encoder pin void PinA() { cli(); //stop interrupts happening before we read pin values reading = PIND & 0xC; // read all eight pin values then strip away all but pinA and pinB's values if (reading == B00001100 && aFlag) { //check that we have both pins at detent (HIGH) and that we are expecting detent on this pin's rising edge encoderPos --; //decrement the encoder's position count bFlag = 0; //reset flags for the next turn aFlag = 0; //reset flags for the next turn } else if (reading == B00000100) bFlag = 1; //signal that we're expecting pinB to signal the transition to detent from free rotation sei(); //restart interrupts }
//Rotary encoder interrupt service routine for the other encoder pin void PinB() { cli(); //stop interrupts happening before we read pin values reading = PIND & 0xC; //read all eight pin values then strip away all but pinA and pinB's values if (reading == B00001100 && bFlag) { //check that we have both pins at detent (HIGH) and that we are expecting detent on this pin's rising edge encoderPos ++; //increment the encoder's position count bFlag = 0; //reset flags for the next turn aFlag = 0; //reset flags for the next turn } else if (reading == B00001000) aFlag = 1; //signal that we're expecting pinA to signal the transition to detent from free rotation sei(); //restart interrupts } // end of sketch!