DEV Community

Cover image for Control a DMX light fixture using an ESP32 over bluetooth
Jens
Jens

Posted on • Edited on

1

Control a DMX light fixture using an ESP32 over bluetooth

This project is a simple example of how to control a DMX light fixture using an ESP32 over bluetooth. The ESP32 will receive the DMX data over bluetooth and send it to the DMX light fixture using the RS485 module.

Introduction

I aimed to control a DMX light fixture using serial communication and React Native for a project. After a few days of research, I found out that serial communication is not ideal for React Native, so I decided to use an ESP32 to connect through Bluetooth and let the ESP32 handle the serial communication to the DMX light fixture.

The RS485 module is needed because the DMX data is transmitted using the EIA-485 protocol also known as RS-485, and the ESP32 doesn't have a RS-485 interface.

The light fixtures used in this example are four LED PARty TCL Spots from Eurolite. Below is a DMX chart illustrating the DMX channels assigned to each light fixture and their corresponding functions.

DMX chart

Each channel controls a specific feature of the light fixture, such as color, intensity, or movement, allowing precise adjustments for dynamic lighting effects.

ESP32

The ESP32 is a low-cost, low-power microcontroller with integrated Wi-Fi and dual-mode Bluetooth. It is a great choice for IoT projects because it has a lot of features and is easy to program. The ESP32 is programmed using the Arduino IDE, a user-friendly platform specifically designed for microcontroller projects. Its extensive library support and simple interface make it ideal for IoT applications, enabling developers to prototype and deploy projects efficiently.

esp_dmx

In this project, I use the esp_dmx library to send DMX data to the light fixture. The esp_dmx library is a simple library that allows you to send DMX data using the RS-485 protocol. The library is easy to use and has a lot of examples that show you how to use it.

Hardware

To build this project, you will need the following hardware:

  • 1x an ESP32 development board (I used the ESP32 WROOM-32D) This microcontroller serves as the brain of the project, enabling Bluetooth communication and DMX signal generation.
  • 1x Blue LED Used as a status indicator to show the Bluetooth connection status or data activity.
  • 1x 220 Ohm Resistor Limits current to the Blue LED, preventing damage.
  • 1x RS485 Module Converts the ESP32's UART signals to the DMX512 protocol, essential for controlling DMX fixtures.
  • 1x Female XLR/DMX Connector Interfaces with the DMX-compatible light fixture via a standard DMX cable. -** 1x USB-C cable** Provides power to the ESP32 and allows for programming.
  • Some jumper wires
  • DMX compatible light fixture The output device that responds to DMX signals sent by the ESP32.
  • XLR/DMX cable To connect the ESP32 to the light fixture

Wiring diagram

Software

To build this project, you will need the arduino IDE. When you have the Arduino IDE installed, you will first need to install the ESP32 core package. Open the BOARDS MANAGER from the menu on the left and search for ESP32. Install the ESP32 by Espressif Systems package.

Arduino IDE BOARDS MANAGER

I've had some compatibility issues with the esp_dmx library and the ESP32, so I had to downgrade the ESP32 core package to version 2.0.17.

Arduino IDE BOARDS MANAGER - Downgrade to 2.0.17
Do the same if you encounter the following error:

esp-dmx_ble.ino:92:52: error: conversion from 'String' to non-scalar type 'std::string' {aka 'std::__cxx11::basic_string<char>'} requested
   92 |     std::string rxValue = pCharacteristic->getValue();
      |                           ~~~~~~~~~~~~~~~~~~~~~~~~~^~

exit status 1

Compilation error: conversion from 'String' to non-scalar type 'std::string' {aka 'std::__cxx11::basic_string<char>'} requested
Enter fullscreen mode Exit fullscreen mode

After installing the board, plug in your ESP and select the board and port in the Arduino IDE.
Arduino IDE Board and Port

To install the esp_dmx library, go to the LIBRARY MANAGER and search for esp_dmx. Install the esp_dmx library by Mitch Weisbrod.
Arduino IDE LIBRARY MANAGER

Code

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include "esp_dmx.h"
Enter fullscreen mode Exit fullscreen mode

First, we include the necessary libraries. For the bluetooth part, we use the BLEDevice, BLEUtils, and BLEServer libraries. For the DMX part, we use the esp_dmx library.

// BLE Configuration
#define LED_PIN 2
#define SERVICE_UUID "afe16d0c-ce27-4ffb-8943-5c3228cffabb"
#define CHARACTERISTIC_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"

// DMX Configuration
const dmx_port_t dmx_num = DMX_NUM_1;
const int TX_PIN = 16; // DI pin
const int RX_PIN = -1; // Not used since we're only transmitting
const int EN_PIN = 21; // DE & RE pins
Enter fullscreen mode Exit fullscreen mode

Next, we set up the configuration for the bluetooth and DMX parts. The LED_PIN is the pin that we use to control the blue LED, this will be used to indicate the status of the bluetooth connection. The SERVICE_UUID and CHARACTERISTIC_UUID are the UUIDs that we use to identify the bluetooth service and characteristic.
The dmx_num is the DMX port that we use, in this case, we use DMX_NUM_1.
TX_PIN: The pin that we use to transmit the DMX data.
RX_PIN: The pin that we use to receive the DMX data, in this case, we don't use it since we're only transmitting.
EN_PIN: - The pin that we use to enable the the Receiver (RE) and Driver (DE) pins of the RS485 module.

// BLE variables
BLEServer *pServer = NULL;
BLEService *pService = NULL;
BLECharacteristic *pCharacteristic = NULL;
bool deviceConnected = false;
bool oldDeviceConnected = false;
Enter fullscreen mode Exit fullscreen mode

We declare the BLE variables that we use to create the bluetooth service and characteristic. We also declare the deviceConnected and oldDeviceConnected variables that we use to keep track of the bluetooth connection status.

// DMX variables
uint8_t dmxData[DMX_PACKET_SIZE] = {0};

const unsigned long DMX_TRANSMIT_INTERVAL = 25; // 40Hz refresh rate
unsigned long lastDmxTransmit = 0;
Enter fullscreen mode Exit fullscreen mode

dmxData: The array that we use to store the DMX data that we receive over bluetooth.
DMX_TRANSMIT_INTERVAL: The interval at which we send the DMX data to the light fixture, in this case, we send the data every 25ms (40Hz refresh rate).
lastDmxTransmit: The time when we last sent the DMX data.

class MyServerCallbacks : public BLEServerCallbacks
{
  void onConnect(BLEServer *pServer)
  {
    deviceConnected = true;
  }
  void onDisconnect(BLEServer *pServer)
  {
    deviceConnected = false;
  }
};
Enter fullscreen mode Exit fullscreen mode

To monitor the bluetooth connection status, we create a class called MyServerCallbacks that extends the BLEServerCallbacks class. We override the onConnect and onDisconnect methods to set the deviceConnected variable to true or false when the device connects or disconnects.

class MyCallbacks : public BLECharacteristicCallbacks
{
  void onWrite(BLECharacteristic *pCharacteristic)
  {
    std::string rxValue = pCharacteristic->getValue();
    if (rxValue.length() > 0)
    {

      for (size_t i = 0; i < rxValue.length() - 1; i += 2)
      {
        uint8_t channel = rxValue[i];
        uint8_t value = rxValue[i + 1];
        if (channel < DMX_PACKET_SIZE)
        {
          dmxData[channel] = value;
        }
      }

      // Blink LED to indicate received data
      digitalWrite(LED_PIN, HIGH);
      delay(10);
      digitalWrite(LED_PIN, LOW);
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

To receive the DMX data over bluetooth, we create a class called MyCallbacks that extends the BLECharacteristicCallbacks class. This way we can override the onWrite method to read the data that we receive over bluetooth and store it in the dmxData array. We also blink the LED to indicate that we received data.

void setup()
{
  // Setup BLE
  BLEDevice::init("DMX Controller");
  pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());

  pService = pServer->createService(SERVICE_UUID);
  pCharacteristic = pService->createCharacteristic(
      CHARACTERISTIC_UUID,
      BLECharacteristic::PROPERTY_READ |
          BLECharacteristic::PROPERTY_WRITE);

  pCharacteristic->setCallbacks(new MyCallbacks());
  pService->start();
  pServer->getAdvertising()->start();

  // Setup DMX
  dmx_config_t config = DMX_CONFIG_DEFAULT;
  dmx_driver_install(dmx_num, &config, NULL, 0);
  dmx_set_pin(dmx_num, TX_PIN, RX_PIN, EN_PIN);

  pinMode(LED_PIN, OUTPUT);
}
Enter fullscreen mode Exit fullscreen mode

In the setup function, we initialize the BLE device and create the bluetooth service and characteristic. We also create the DMX service and set the pins that we use to transmit the DMX data. We also set the LED_PIN as an output pin.

void loop()
{
  // Handle BLE connection status
  if (!deviceConnected && oldDeviceConnected)
  {
    delay(500);
    pServer->startAdvertising();
    oldDeviceConnected = deviceConnected;
  }

  if (deviceConnected && !oldDeviceConnected)
  {
    oldDeviceConnected = deviceConnected;
  }

  // Continuously send DMX data at regular intervals
  unsigned long currentMillis = millis();
  if (currentMillis - lastDmxTransmit >= DMX_TRANSMIT_INTERVAL)
  {
    lastDmxTransmit = currentMillis;
    dmx_write(dmx_num, dmxData, DMX_PACKET_SIZE);
    dmx_send(dmx_num);
    dmx_wait_sent(dmx_num, DMX_TIMEOUT_TICK);
  }

  // Blink LED when not connected
  if (!deviceConnected)
  {
    digitalWrite(LED_PIN, HIGH);
    delay(200);
    digitalWrite(LED_PIN, LOW);
    delay(200);
  }
}
Enter fullscreen mode Exit fullscreen mode

In the loop function, we handle the BLE connection status and send the DMX data to the light fixture at regular intervals. We also blink the LED when the device is not connected.

For optional debugging, you can add the following code to the project:

#define DEBUG true // Set to false to disable debug printing

void printDmxDebug(uint8_t channel, uint8_t value)
{
  if (!DEBUG)
    return;

  Serial.print("DMX Channel ");
  if (channel < 10)
    Serial.print("00");
  else if (channel < 100)
    Serial.print("0");
  Serial.print(channel);
  Serial.print(" set to ");
  if (value < 100)
    Serial.print(" ");
  if (value < 10)
    Serial.print(" ");
  Serial.print(value);
  Serial.print(" (");
  Serial.print(float(value) / 255.0 * 100.0, 1);
  Serial.println("%)");
}

/* Change the MyCallbacks class to this: */
class MyCallbacks : public BLECharacteristicCallbacks
{
  void onWrite(BLECharacteristic *pCharacteristic)
  {
    std::string rxValue = pCharacteristic->getValue();
    if (rxValue.length() > 0)
    {

      if (DEBUG)
      {
        Serial.println("\n--- New DMX Values Received ---");
      }

      for (size_t i = 0; i < rxValue.length() - 1; i += 2)
      {
        uint8_t channel = rxValue[i];
        uint8_t value = rxValue[i + 1];
        if (channel < DMX_PACKET_SIZE)
        {
          dmxData[channel] = value;
          printDmxDebug(channel, value);
        }
      }

      if (DEBUG)
      {
        Serial.println("---------------------------\n");
      }

      // Blink LED to indicate received data
      digitalWrite(LED_PIN, HIGH);
      delay(10);
      digitalWrite(LED_PIN, LOW);
    }
  }
};

/* Add this to the setup function: */
Serial.begin(115200);
Enter fullscreen mode Exit fullscreen mode

This way you can see the DMX data that you receive over bluetooth in the serial monitor, which can be useful for debugging.

Full code

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include "esp_dmx.h"

// BLE Configuration
#define LED_PIN 2
#define SERVICE_UUID "afe16d0c-ce27-4ffb-8943-5c3228cffabb"
#define CHARACTERISTIC_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"

// DMX Configuration
const dmx_port_t dmx_num = DMX_NUM_1;
const int TX_PIN = 16; // DI pinz
const int RX_PIN = -1; // Not used since we're only transmitting
const int EN_PIN = 21; // DE & RE pins

// BLE variables
BLEServer *pServer = NULL;
BLEService *pService = NULL;
BLECharacteristic *pCharacteristic = NULL;
bool deviceConnected = false;
bool oldDeviceConnected = false;

// DMX variables
uint8_t dmxData[DMX_PACKET_SIZE] = {0};

const unsigned long DMX_TRANSMIT_INTERVAL = 25; // 40Hz refresh rate
unsigned long lastDmxTransmit = 0;

#define DEBUG true // Set to false to disable debug printing

void printDmxDebug(uint8_t channel, uint8_t value)
{
  if (!DEBUG)
    return;

  Serial.print("DMX Channel ");
  if (channel < 10)
    Serial.print("00");
  else if (channel < 100)
    Serial.print("0");
  Serial.print(channel);
  Serial.print(" set to ");
  if (value < 100)
    Serial.print(" ");
  if (value < 10)
    Serial.print(" ");
  Serial.print(value);
  Serial.print(" (");
  Serial.print(float(value) / 255.0 * 100.0, 1);
  Serial.println("%)");
}

class MyServerCallbacks : public BLEServerCallbacks
{
  void onConnect(BLEServer *pServer)
  {
    deviceConnected = true;
  }
  void onDisconnect(BLEServer *pServer)
  {
    deviceConnected = false;
  }
};

/*
Example byte array format for sending DMX data over BLE:
For a simple RGB light starting at DMX address 1:
[1, 255,    // Channel 1 (Red) = 255
 2, 128,    // Channel 2 (Green) = 128
 3, 64]     // Channel 3 (Blue) = 64

For multiple lights:
[1, 255,    // Light 1: Red = 255
 2, 128,    // Light 1: Green = 128
 3, 64,     // Light 1: Blue = 64
 4, 255,    // Light 1: Dimmer = 255
 5, 0,      // Light 1: Strobe = 0
 6, 255,    // Light 2: Red = 255
 7, 0,      // Light 2: Green = 0
 8, 0]      // Light 2: Blue = 0

Each pair represents: [DMX_Channel, Value]
Channel: 1-512
Value: 0-255
*/

class MyCallbacks : public BLECharacteristicCallbacks
{
  void onWrite(BLECharacteristic *pCharacteristic)
  {
    std::string rxValue = pCharacteristic->getValue();
    if (rxValue.length() > 0)
    {

      if (DEBUG)
      {
        Serial.println("\n--- New DMX Values Received ---");
      }

      for (size_t i = 0; i < rxValue.length() - 1; i += 2)
      {
        uint8_t channel = rxValue[i];
        uint8_t value = rxValue[i + 1];
        if (channel < DMX_PACKET_SIZE)
        {
          dmxData[channel] = value;
          printDmxDebug(channel, value);
        }
      }

      if (DEBUG)
      {
        Serial.println("---------------------------\n");
      }

      // Blink LED to indicate received data
      digitalWrite(LED_PIN, HIGH);
      delay(10);
      digitalWrite(LED_PIN, LOW);
    }
  }
};

void setup()
{
  Serial.begin(115200);

  // Setup BLE
  BLEDevice::init("DMX Controller");
  pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());

  pService = pServer->createService(SERVICE_UUID);
  pCharacteristic = pService->createCharacteristic(
      CHARACTERISTIC_UUID,
      BLECharacteristic::PROPERTY_READ |
          BLECharacteristic::PROPERTY_WRITE);

  pCharacteristic->setCallbacks(new MyCallbacks());
  pService->start();
  pServer->getAdvertising()->start();

  // Setup DMX
  dmx_config_t config = DMX_CONFIG_DEFAULT;
  dmx_driver_install(dmx_num, &config, NULL, 0);
  dmx_set_pin(dmx_num, TX_PIN, RX_PIN, EN_PIN);

  pinMode(LED_PIN, OUTPUT);
}

void loop()
{
  // Handle BLE connection status
  if (!deviceConnected && oldDeviceConnected)
  {
    delay(500);
    pServer->startAdvertising();
    oldDeviceConnected = deviceConnected;
  }

  if (deviceConnected && !oldDeviceConnected)
  {
    oldDeviceConnected = deviceConnected;
  }

  // Continuously send DMX data at regular intervals
  unsigned long currentMillis = millis();
  if (currentMillis - lastDmxTransmit >= DMX_TRANSMIT_INTERVAL)
  {
    lastDmxTransmit = currentMillis;
    dmx_write(dmx_num, dmxData, DMX_PACKET_SIZE);
    dmx_send(dmx_num);
    dmx_wait_sent(dmx_num, DMX_TIMEOUT_TICK);
  }

  // Blink LED when not connected
  if (!deviceConnected)
  {
    digitalWrite(LED_PIN, HIGH);
    delay(200);
    digitalWrite(LED_PIN, LOW);
    delay(200);
  }
}
Enter fullscreen mode Exit fullscreen mode

Testing

After uploading the code to the ESP32, you can connect to the ESP32 using a bluetooth debug app like nRF Connect. You can then send the DMX data to the ESP32 using the app and control the light fixture. You can also use the serial monitor in the Arduino IDE to see the DMX data that you receive over bluetooth if you added the optional debugging code.

How to send DMX data over bluetooth

To send DMX data over bluetooth, you need to send a byte array with the DMX channel and value pairs. For example, to set the red and dimmer channels to 255, you would send the following byte array: 01FF04FF

Result with nRF Connect example

End result

For the end product I designed a custom case that holds the ESP32, RS485 module, led, and XLR connector. The case is 3D printed and held togheter with magnets for easy access to the ESP32 and RS485 module. The XLR connector is mounted on the front of the case and the USB port of the ESP is accessible from the back of the case, with a hole for the LED.

3D printed case

Some pictures of the end result

end result

Do your career a big favor. Join DEV. (The website you're on right now)

It takes one minute, it's free, and is worth it for your career.

Get started

Community matters

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Dive into an ocean of knowledge with this thought-provoking post, revered deeply within the supportive DEV Community. Developers of all levels are welcome to join and enhance our collective intelligence.

Saying a simple "thank you" can brighten someone's day. Share your gratitude in the comments below!

On DEV, sharing ideas eases our path and fortifies our community connections. Found this helpful? Sending a quick thanks to the author can be profoundly valued.

Okay