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.

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!