Converting Light Cubes to DMX

We’re converting the balloon light boxes that Shaun M. donated to be WiFi ArtNET/DMX light fixtures.

Initial Hardware

  • custom ST32F103CB CPU on the custom computer board. It is a 3.6V CPU
  • it has a XBee Pro S1 RF module which is a 3.6V device
  • buffer between ST CPU and outputs to PWM driver boards is a HC245 octal bus transceiver, looks to be capable of supplying up to 50mA
  • a power supply board that we suspect outputs up to 18V for the 6 series connected RGB LEDs in the top ring
  • a 2nd Power supply for the 12V LED strips.
    • it uses 3x 843ON55 power MOSFETs (12A, 60V, N-Channel)
  • includes a mains to 12V PSU
  • NEMA 23 stepper (KL23H276-30-8B)
  • LeadShine DM556 stepper controller
  • solenoid locking pin

LED Driver circuit



Four wire connector from the main board

  • red wire = ground, yellow = blue LEDs, white = green LEDs, black = red LEDs

Day 1: Plan and Reverse Engineering

October 15, 2019


  • We will use an ESP266 and add DMX/ArtNET software to drive 3 PWM signals.

  • We’ll use those to drive the existing 12V LED power supply board.

  • We will remove the custom CPU board as we have no documentation about it.

  • We will also remove the stepper and driver and repurpose those for another project.

  • We will also have to re-wire the top ring of 6 LEDs to be 12V compatible.

  • Most of the first session was spent looking at the components (under magnification) to determine the components used, guess at the circuits; general reverse engineering. The information found was added to the initial project description post.

Day 2: Function Generators and Oscilloscopes

October 22, 2019



Tasks completed

  • more finding and checking of component spec sheets, and updating the component description.

  • used the oscilloscope and function generator to create a single sided (0 -> +ve voltage) square wave of variable frequency

  • disconnected the main CPU board and disconnected the stepper driver controller so they don’t interfere

  • figured out how the LED power control circuit gets it’s power (via the connector to the main CPU board!)

  • sent a square wave to the LED driver- got blinking lights!

  • for the four wire connector from the main board: red wire = ground, yellow = blue LEDs, white = green LEDs, black = red LEDs

  • discovered the importance of double checking connections; ground wasn’t connected properly at one point, and we’ve destroyed the red driver transistor on one of the LED driver boards. Luckily we have some spares

  • (Julian) started looking into how best to drive the MOSFETs from a 3.3V micro controller

Day 3: Software

October 30, 2019

Today’s goal is to be able to program an ESP8266 using the Arduino development environment.

We will use an Adafruit Feather Huzzah ESP8266.

Adafruit has a guide for setting up development environments for this board, including Arduino. We followed its instructions.

There is also the Arduino for ESP8266 programming documentation which is very useful.

At the end we succeeded in creating and downloading a simple blink sketch, and it worked!

Day 4: Starting DMX

November 5th, 2019

Some useful background on ArtNET:

Basic ArtNET sketch:

Another ArtNET sketch:

And another: - source:

Advanced ArtNET controller v1.x:

Advanced ArtNET controller v2.x:

Code for last two:

Day 5: Art-Net

Attempted to send Art-Net packets to a pc using an Adafruit feather huzzah, It failed. (Probably the fault of a human, not the software) The Arduino library being used is this Art-Net library by hideakitai.

Also looked into arrays and other confusions of c++.

GitHub repository
Q Light Controller+, the controller software being used

Day 6 - Prep

  • document QLC+ requirement for full ArtNET/DMX packets
  • configuring virtual console to see inputs
  • Awesome ArtNET debugging tool ( - find a windows equivalent
  • investigate WiFi error codes, and how to catch typos/issues as per what happened in Day 5.

Day 6: programming issues

Fixed an error in the tx code, 512 does not in fact fit in 8 bits, but it does fit in 16, I’m a spoon.

ArtNetominator artnet debugger
We still have yet to figure out the wifi error codes, so typos still won’t be caught.
Probably going to start on the rx code soon, that’ll be a headache.

Day 7: “Hello, are you there?” (Receiving Messages)

Got some rx code working,you can find it here: GitHub Art-Net rx repo

Edit: we also found a few QLC+ bugs, in order to receive artnet packets you need to enable both input and output.

Day 8: WiFi Manager

Switched the rx code’s wifi library to this WiFi manager library by tzapu, the updated code can be found on the GitHub repo. It has been tested, and it works. (Note: you currently have to use the serial monitor to get the ip address)

Day 9

Made it easy to reset wifi settings, all changes are on the github repo.
Also started work on the AP names.

Day 10

After much confusion, we managed to give the esp8266s unique AP names, based on the unique chip id, fetched with ESP.getChipId(). I also learned a bit about compiled vs interpreted languages, that self modifying code can be problematic, here’s a relevant link: even learned exactly why my idea didn’t work. GitHub: here.

Day 11: Custom Configuration

Began implementing a file system and config file. Learned about null terminated strings, and that it isn’t a good idea to store things with zeroes in them, as a 0 will end the string. The code desperately needs to be updated from arduinojson 5 to arduinojson 6.

The unique AP name appears to be working quite well.

The config file currently only saves the universe, that will be changed.

Links to more information:
GitHub repo
Arduino json update

Day 12: Custom Config Parameters

How to add descriptive text labels to custom parameters in WiFi manager: github issue

Another day 12

Moved everything to a new breadboard, it's all working.

Got chasers working in qlc+, it was only a small headache. The chaser is pretty simple, more of a test than anything.

All the changes can be found on the GitHub repo.

The green led seems to be brighter than the rest.

Setting Up QLC

  1. Add ESP8266 To QLC

Go to inputs/outputs. Add an ArtNet item. Make sure it is set as an output. Double click it, and set the IP correctly.

Note: to be a receiver, we and to enable both input and output. Also need to make sure you’re sending full DMX frames. It didn’t like partial frames.

  1. Add the Fixture(s)

Got to Fixtures tab.
Add a custom fixture with the appropriate channels for the ESP8266 setup.

  1. Basic Test

Go to the Simple Desk. The fixture should show on the sliders part of the window. Moving the sliders should make the LEDs change. Make sure the colour you expect changes when you move the slider.

  1. To Make a Chaser

Go to the functions tab.

Make a scene. Add the fixture(s) to the scene.

Set the sliders as you would like for this scene, or step.

Note: this be live previewed on the devices (fixtures.)

Then add a new chaser.

Add the scenes you want in this chaser, and edit the in/out and hold.

You can also preview your chaser.

  1. Add Chaser to Virtual Console

The virtual console is the command centre for a live performance.

You can add your chaser as a button.

Don’t forget to be in run mode!!!

This is a place holder because Discourse won’t allow more than 3 consecutive replies from the same person. This is something that Julian should look into!

Day… 14

Homework: What is a class? What is an object? What is an instance?

An object is a thing that might represent something in the real world. e.g. a Pet.
A class is scaffolding or a template, that describes the required data for the object, and it’s behaviours. Behaviours are usually verbs. an instance is the scaffolding or template filled out.

The class needs to be made first, with all the variables and methods declared, and everything has to have it’s accessibility (from outside, or subclasses) defined. A class usually doesn’t have any memory allocated to the methods or variables.

Once the class has been made, an object can be created from it(using ClassName ObjectName;), and then the methods can be called with ObjectName.FunctionName();,
and the variables can be accessed with ObjectName.VariableName = aNumber;.

Here is the code for an example class and instance. The class represents a line that will be drawn on a text display. It implements a method to draw a line of ‘#’ between two given points. The code creates an instance and uses it to draw a line.


#include <Arduino.h>

  class OneDRender {

     // Constructors
     OneDRender() {


     void reset() {
       strcpy(output, "________________");

     // Destructor
     ~OneDRender() {};
      void draw(int vertex1, int vertex2) {
        int temp;
        if (vertex1 > vertex2) {
          temp = vertex2;
          vertex2 = vertex1;
          vertex1 = temp;
        for (int i = vertex1; i <= vertex2; i++)
          output[i] = '#';

      void render() {

      char output[17];
  }; //class end
  OneDRender render;

void setup() {

  //OneDRender render;
  for (int i = 0; i < 12; i++)
    //OneDRender render;
    render.draw(i, i+4);
  render.draw(0, 5);
  render.draw(11, 16);


 void loop() {
  // put your main code here, to run repeatedly:
  for (int i = 0; i < 12; i++)
    //OneDRender render;
    render.draw(i, i+4);

This is now on github.

Day… 14? 15?

This piece of rather unoptimised code is supposed to take two
points and draw a line between them, although unfortunately I have not implemented any line drawing algorithms, so it only does text.(render.txtdraw(xpos, ypos, “textToPrint”); render.render(); )

#include <Arduino.h>
#include <SoftwareSerial.h>

//these variables indicate the dimensions of the display
const int width = 16;
const int height = 16;

class TwoDRender
//this char array contains the output of the renderer, and it is 2-dimensional (like the renderer output)
  char output[width][height];
  int tmp1;
  int tmp2;
  char lineSwap1[17];
  char lineSwap2[17];
  void draw(int vertex1x, int vertex1y, int vertex2x, int vertex2y)
    //this is the main render code lol

//the function that allows text to be printed
  int txtdraw(int x, int y, char text[17]) {
//error checking, if the input text does not fit on the display, it will return -1, to show that it does not fit
    if(x >= width || x < 0 || y >= height || y < 0) {
      return -1;
//this actually puts the text in the output variable
    for (int i = 0; (i + x) < width && text[i] != 0; i++)
      output[i + x][y] = text[i];
    return 0;

  void render() {
//this outputs the contents of the output variable to the display
    for (int y = 0; y < width; y++)
      for (int x = 0; x < height; x++)

  void reset() {
//the reset function, simply here to set the entire output to underscores when called
    for (int x = 0; x < width; x++)
      for (int y = 0; y < height; y++)
        output[x][y] = '_';

  ~TwoDRender() {};

TwoDRender render;

void setup() {
  render.txtdraw(3, 0, "Test");
  render.txtdraw(12, 4, "truncate"); //this doesn't actually fit on the display

void loop() {
  // put your main code here, to run repeatedly:

This is now on github.

Day 15 (i think)

Implemented the option to change the art-net channels that are listened to, although the red channel doesn’t seem to be able to change.

#include <FS.h>
#include <ArduinoJson.h>

#include <ESP8266WiFi.h>          //ESP8266 Core WiFi Library

#include <DNSServer.h>            //Local DNS Server used for redirecting all requests to the configuration portal
#include <ESP8266WebServer.h>     //Local WebServer used to serve the configuration portal
#include <WiFiManager.h>          // WiFi Configuration Magic

#include <SoftwareSerial.h>
#include <EEPROM.h>

#include <Artnet.h>

const int pwmMax = 255;

int ledOnboard = 0; //onboard LED pin
char ledOnboardChar[3]; //ditto char
int ledOnboardIn = 0; //onboard LED intake channel
char ledOnboardInChar[4];
int Rled = 12; //red led pin
char RledChar[3] = "12";
int Rin = 1; //red intake channel
char RinChar[4] = "1";
int Gled = 13; //green led pin
char GledChar[3];
int Gin = 2; //green intake channel
char GinChar[4];
int Bled = 14; //blue led pin
char BledChar[3];
int Bin = 3; //blue intake channel
char BinChar[4];

const int resetSwitch = 5;

bool shouldSaveConfig = false; //flag for saving data

// IP stuffs
IPAddress ip;

ArtnetReceiver artnet;
char universeChar[6] = "1";
uint16_t universe = 1; //artnet universe
const uint32_t universe2 = 2;

void artNetCallback(uint8_t* data, uint16_t size)
    // you can also use pre-defined callbacks

//callback notifying us of the need to save config
void saveConfigCallback () {
  Serial.println("Should save config");
  shouldSaveConfig = true;

void readConfigFile() {
  Serial.println("mounting FS...");

  if (SPIFFS.begin()) {
    Serial.println("mounted file system");
    if (SPIFFS.exists("/config.json")) {
      //file exists, reading and loading
      Serial.println("reading config file");
      File configFile ="/config.json", "r");
      if (configFile) {
        Serial.println("opened config file");
        size_t size = configFile.size();
        // Allocate a buffer to store contents of the file.
        std::unique_ptr<char[]> buf(new char[size]);

        configFile.readBytes(buf.get(), size);
        DynamicJsonBuffer jsonBuffer;
        JsonObject& json = jsonBuffer.parseObject(buf.get());
        if (json.success()) {
          Serial.println("\nparsed json");
          universe =  json["universe"];
          ledOnboard = json["ledOnboard"];
          ledOnboardIn = json["ledOnboardIn"];
          Rled = json["Rled"];
          Rin = json["Rin"];
          Gled = json["Gled"];
          Gin = json["Gin"];
          Bled = json["Bled"];
          Bin = json["Bin"];
        } else {
          Serial.println("failed to load json config");
  } else {
    Serial.println("failed to mount FS");
  //end read

void setup() {
    pinMode (resetSwitch, INPUT_PULLUP);
    pinMode (ledOnboard, OUTPUT); //led output declarations
    pinMode (Rled, OUTPUT);
    pinMode (Gled, OUTPUT);
    pinMode (Bled, OUTPUT);


    // WiFi stuffs
    WiFiManagerParameter artNetUniverse("universe", "artnet universe", universeChar, 6);
    WiFiManagerParameter artNetUniverseLabel("<p>Art-Net universe</p>");
    WiFiManagerParameter RledIn("Rin", "Red art-net channel(default 1)", RinChar, 6);
    WiFiManagerParameter RledInLabel("<p>Red art-net channel</p>");
    WiFiManager wifiManager;

    if(digitalRead(resetSwitch) == LOW) { //reset
      bool ledState = true;
      Serial.println("resetting wifi...");
      for(int i = 0; i < 10; i++) {
        digitalWrite(Gled, ledState);
        ledState = !ledState;

    String apName = String("lightbox" + String(ESP.getChipId()));
    Serial.println("apName = " + apName);

    strcpy(universeChar, artNetUniverse.getValue()); //start save
    strcpy(RinChar, RledIn.getValue());
    Rin = atoi(RinChar);
    if (shouldSaveConfig) {
    Serial.println("saving config");
    DynamicJsonBuffer jsonBuffer;
    JsonObject& json = jsonBuffer.createObject();
    json["universe"] = universeChar;
    json["ledOnboard"] = ledOnboard;
    json["ledOnboardIn"] = Rin - 1;
    json["Rled"] = Rled;
    json["Rin"] = Rin;
    json["Gled"] = Gled;
    json["Gin"] = Rin + 1;
    json["Bled"] = Bled;
    json["Bin"] = Rin + 2;

    File configFile ="/config.json", "w");
    if (!configFile) {
      Serial.println("failed to open config file for writing");

    //end save
    ip = WiFi.localIP();


    // if Artnet packet comes to this universe, this function (lambda) is called
    artnet.subscribe(universe, [&](uint8_t* data, uint16_t size)
        Serial.print("lambda : artnet data (universe : ");
        Serial.println(") = ");
        analogWrite(ledOnboard, pwmMax - data[ledOnboardIn]); //write to LEDs
        analogWrite(Rled, data[Rin]);
        analogWrite(Gled, data[Gin]);
        analogWrite(Bled, data[Bin]);
        for (size_t i = 0; i < 4; ++i) //the 4 on this line is how many channels are sent to the serial monitor, you can change it to be whatever you want, but if it is too big everything will grind to a halt.
            //Serial.print(i); Serial.print(","); Serial.print(data[i]); Serial.print(",");

    //you can also use pre-defined callbacks
    //artnet.subscribe(universe2, artNetCallback);

bool doOnce = false;

void loop(){
  if (!doOnce)
    doOnce = true;
    artnet.parse(); // check if artnet packet has come and execute callback

this is on the github repo