Turning an Ultra DMX Micro into an Art-Net node

From a previous project, I have some unused Raspberry Pis lying around gathering dust. I also have a DMX King Ultra DMX Micro that I normally attach to my laptop for controlling DMX devices. Since the DMX dongle is compatible with Linux, it should in theory be possible to connect the two together and control my DMX stuff with a Pi. Then I got the idea to write a Processing sketch that runs on the Pi, receiving Art-Net and passing it on to the DMX dongle. A custom Art-Net node. Gone will be the days of long DMX runs from FOH to the stage! Long live the single ethernet cable for both light, LED strips and lasers!

The device. A simple Raspberry Pi with the Ultra DMX Micro attached.

Requirements for this project

  • Device sending Art-Net (I used a laptop and tested with Freestyler, QLC+ and Onyx)
  • Device receiving Art-Net (I used a Raspberry Pi)
  • USB to DMX device compatible with the Enttec USB Pro protocol (I used a DMX King Ultra DMX Micro)
  • Network gear: router, cables, …

Testing the concept

Let’s start with writing a very simple Processing sketch that sends DMX data from the dongle. Processing has a third-party library for controlling any DMX controller implementing the Enttec DMX USB Pro protocol. This includes the Ultra DMX Micro. Modifying the “basic” example so it controls my scanner’s movement with the mouse:

import dmxP512.*;
import processing.serial.*;

DmxP512 dmxOutput;
int universeSize=128;


String DMXPRO_PORT;//case matters ! on windows port must be upper cased.
int DMXPRO_BAUDRATE=115000;

void setup() {

  size(245, 245, JAVA2D);  

  for (String s : Serial.list())
  {
    if (s.contains("USB")) DMXPRO_PORT = s;
  }

  dmxOutput=new DmxP512(this, universeSize, false);
  dmxOutput.setupDmxPro(DMXPRO_PORT, DMXPRO_BAUDRATE);
}

void draw() {    
  background(0);

  dmxOutput.set(1, (int)map(mouseX, 0, width, 0, 255)); //channel 1: pan
  dmxOutput.set(2, (int)map(mouseY, 0, height, 0, 255)); //channel 2: tilt
  dmxOutput.set(3, 255);
  dmxOutput.set(4, (int)(frameCount/10)%127);
}

It works! Great success!

Scanner is working! Very nice! I like!

Receiving Art-Net

Processing also has an Art-Net library. It has an example that receives Art-Net and results in a byte array with the DMX values. I simply combined the two examples so that the Art-Net is passed onto the dmxOutput object. 

There were a couple of subleties involved. One is that DMX channel 0 is reserved and no data should be sent to it. Art-Net doesn’t have that. This means that Art-Net channel 0 should be remapped to DMX channel 1, and so on. Also, DMX values are unsigned bytes (0-255), but Processing treats bytes as signed by default. So, when sending the data, “&0xff” needs to be added to the byte to make it unsigned.

Now Freestyler running on my laptop controls the sketch!

When those issues were solved, the code works really well and is stable. I’ve ran it for a week now without any issues or crashes.

import dmxP512.*;
import processing.serial.*;
import ch.bildspur.artnet.*;

ArtNetClient artnet;



//byte array that will contain the incoming art-net message
byte[] dmxData = new byte[512];

//the dmx device
//DmxP512 dmxOutput;

//String DMXPRO_PORT;//case matters ! on windows port must be upper cased.
int DMXPRO_BAUDRATE=115000;

byte[] dataBuffer = new byte[512];

ArrayList<DmxOutput> outputs = new ArrayList();

int outputHeight = 70;

StringList status = new StringList();

void setup() 
{

  size(800, 600, JAVA2D);  
  surface.setResizable(true);

  int y = 0;

  //find the connected devices, if on Raspi, the device will be called "/dev/ttyUSBX" which we can use to distinguish it from the other serial devices
  //on windows, there should be only one (if you only have the dmx dongle connected)
  for (String s : Serial.list())
  {
    if (s.contains("USB") || (System.getProperty("os.name").startsWith("Windows"))) outputs.add(new DmxOutput(this, s, 20+y++*outputHeight));
  }

  //initialise the art-net node

  artnet = new ArtNetClient();
  artnet.start();

  status.append("Started. " + outputs.size() + " devices added. Listening for Art-Net...");
}

void draw() {    
  background(0);


  for (DmxOutput output : outputs)
  {
    output.update();
  }

}



class DmxOutput
{
  DmxP512 dmxOut;
  int universe, subnet;
  int universeSize = 512;
  int y;
  boolean error = false;

  DmxOutput(PApplet parent, String s, int y)
  {
    dmxOut = new DmxP512(parent, universeSize, false);
    dmxOut.setupDmxPro(s, DMXPRO_BAUDRATE);
    this.y = y;
  }

  void setUniverse(int universe)
  {
    this.universe = universe;
  }

  void setSubnet(int subnet)
  {
    this.subnet = subnet;
  }

  void update()
  {
    //listen to art-net
    byte[] data = artnet.readDmxData(subnet, universe);  //args: subnet, universe

    //detect a new message
    boolean changed = false;
    for (int i = 0; i < 512; i++)
    {
      if (data[i] != dataBuffer[i])
      {
        changed = true;
        break;
      }
    }
    dataBuffer = data;

    boolean sending = false;

    //only resend dmx if the data has changed
    //also resend every 166 ms to avoid devices losing the dmx connection
    //most devices have a 1 second timeout
    if (changed || frameCount%10==0)
    {
      for (int i = 0; i < universeSize; i++)
      {
        //dmx channel offset: incoming art-net channel 0 will actually be dmx channel 1
        //the data will become signed, but it needs to be unsigned, this can be fixed with the "&0xff" code
        dmxOut.set(i+1, data[i] & 0xff);
        sending = true;
      }
    }

    //some graphical cues for troubleshooting
    if (changed)
    {
      fill(0, 255, 0);
    } else
    {
      fill(255, 0, 0);
    }
    rect(30, y+5, 30, 30);

    if (sending)
    {
      fill(0, 255, 0);
    } else
    {
      fill(255, 0, 0);
    }
    rect(30, y+40, 30, 30);


    fill(255);
    text("Art-Net subnet " + subnet + " universe " + universe, 70, y+20);
  }
}

Some more control over the situation

You might have noticed that instead of having one “DMXP512” object, I encapsulated it in a class with some additional parameters. This will allow me to have multiple outputs connected at the same time, if I ever need to buy more DMX King dongles. This class has “universe” and “subnet” properties, so it can be remapped to other Art-Net universa if required.

I’d like to control these properties remotely. For that, I’ve chosen the OSC protocol, as it is universal and easy to use. The command structure is quite simple.

If the command “/status” is received, it replies with the command “/reply” containing an array of Strings all following this order: “key” “value”. For example: “Devices 1” if one device is connected. It will then follow with the data of all outputs: universe, subnet, universe size ( = amount of channels) and if an error was encountered.

It will also listen for the commands “/universe” and “/subnet”. The body of these messages always has two integers: the first integer specifies the output and the second the actual value. For example, if you want to set the Art-Net universe that the first attached DMX dongle should listen to, to five, use the command “/universe” – “0” – “5”.

import netP5.*;
import oscP5.*;
import dmxP512.*;
import processing.serial.*;
import ch.bildspur.artnet.*;

ArtNetClient artnet;

OscP5 osc;

//byte array that will contain the incoming art-net message
byte[] dmxData = new byte[512];

//the dmx device
//DmxP512 dmxOutput;

//String DMXPRO_PORT;//case matters ! on windows port must be upper cased.
int DMXPRO_BAUDRATE=115000;

byte[] dataBuffer = new byte[512];

ArrayList<DmxOutput> outputs = new ArrayList();

int outputHeight = 70;

StringList status = new StringList();

void setup() 
{

  size(800, 600, JAVA2D);  
  surface.setResizable(true);

  int y = 0;

  //find the connected devices, if on Raspi, the device will be called "/dev/ttyUSBX" which we can use to distinguish it from the other serial devices
  //on windows, there should be only one (if you only have the dmx dongle connected)
  for (String s : Serial.list())
  {
    if (s.contains("USB") || (System.getProperty("os.name").startsWith("Windows"))) outputs.add(new DmxOutput(this, s, 20+y++*outputHeight));
  }

  //initialise the art-net node

  artnet = new ArtNetClient();
  artnet.start();

  osc = new OscP5(this, 9009);

  status.append("Started. " + outputs.size() + " devices added. Listening for Art-Net...");
}

void draw() {    
  background(0);


  for (DmxOutput output : outputs)
  {
    output.update();
  }

  displayStatus();
}

void oscEvent(OscMessage message)
{
  println("Rx'd message from ", message.address().substring(1, message.address().length()), message.port());
  NetAddress replyAddress = new NetAddress(message.address().substring(1, message.address().length()), message.port());
  OscMessage reply = new OscMessage("/reply");
  if (message.checkAddrPattern("/status"))
  {
    status.clear();
    status.append("Status poll request received");
    reply.add("Devices " + outputs.size());
    int i = 0;
    for (DmxOutput output : outputs)
    {
      reply.add("Output " + i++);
      reply.add("Universe " + output.universe);
      reply.add("Subnet " + output.subnet);
      reply.add("Universe size " + output.universeSize);
      reply.add("Error " + output.error);
    }
  } else if (message.checkAddrPattern("/universe"))
  {
    if (message.checkTypetag("ii")) 
    {
      int output = message.get(0).intValue();
      if (output < outputs.size())
      {
        outputs.get(output).universe = message.get(1).intValue();
        reply.add("Set output " + output + " to universe " + outputs.get(output).universe);
      } else
      {
        reply.add("Error: not enough outputs. Connected DMX dongles: " + outputs.size() + ", attempted output: " + output);
      }
    }
    else reply.add("Error: wrong typetag. Expected two integers, got " + message.typetag());
  }
  else if (message.checkAddrPattern("/subnet"))
    {
      if (message.checkTypetag("ii")) 
      {
        int output = message.get(0).intValue();
        if (output < outputs.size())
        {
          outputs.get(output).subnet = message.get(1).intValue();
          reply.add("Set output " + output + " to subnet " + outputs.get(output).subnet);
        } else
        {
          reply.add("Error: not enough outputs. Connected DMX dongles: " + outputs.size() + ", attempted output: " + output);
        }
      }
      else reply.add("Error: wrong typetag. Expected two integers, got " + message.typetag());
    }
    osc.send(reply, replyAddress);
  }

  void displayStatus() //Displays some information, such as the strings in the status arraylist, fps, file header etc
  {
    fill(255);
    textAlign(LEFT);

    for (int i = 0; i < status.size (); i++)
    {
      fill(255);
      if (textWidth(status.get(i)) > width-10) 
      {
        String[] brokenText = splitTokens(status.get(i));
        String str1 = "";
        String str2 = "";
        int k = 0;
        if (!(textWidth(brokenText[0]) > width-10))
        {
          for (int j = 0; j < brokenText.length; j++)
          {      
            if (textWidth(str1 + brokenText[j] + " ") > width-10)
            {

              k = j;
              j = brokenText.length;
            } else str1 += brokenText[j] + " ";
          }
          status.set(i, str1);
          for (int j = k; j < brokenText.length; j++)
          {      
            str2 += brokenText[j] + " ";
          }
          status.insert(i+1, str2);
        }
      }

      text(status.get(i), 10, height-20*status.size()+20*i);
    }
  }
class DmxOutput
{
  DmxP512 dmxOut;
  int universe, subnet;
  int universeSize = 512;
  int y;
  boolean error = false;

  DmxOutput(PApplet parent, String s, int y)
  {
    dmxOut = new DmxP512(parent, universeSize, false);
    dmxOut.setupDmxPro(s, DMXPRO_BAUDRATE);
    this.y = y;
  }

  void setUniverse(int universe)
  {
    this.universe = universe;
  }

  void setSubnet(int subnet)
  {
    this.subnet = subnet;
  }

  void update()
  {
    //listen to art-net
    byte[] data = artnet.readDmxData(subnet, universe);  //args: subnet, universe

    //detect a new message
    boolean changed = false;
    for (int i = 0; i < 512; i++)
    {
      if (data[i] != dataBuffer[i])
      {
        changed = true;
        break;
      }
    }
    dataBuffer = data;

    boolean sending = false;

    //only resend dmx if the data has changed
    //also resend every 166 ms to avoid devices losing the dmx connection
    //most devices have a 1 second timeout
    if (changed || frameCount%10==0)
    {
      for (int i = 0; i < universeSize; i++)
      {
        //dmx channel offset: incoming art-net channel 0 will actually be dmx channel 1
        //the data will become signed, but it needs to be unsigned, this can be fixed with the "&0xff" code
        dmxOut.set(i+1, data[i] & 0xff);
        sending = true;
      }
    }


    if (changed)
    {
      fill(0, 255, 0);
    } else
    {
      fill(255, 0, 0);
    }
    rect(30, y+5, 30, 30);

    if (sending)
    {
      fill(0, 255, 0);
    } else
    {
      fill(255, 0, 0);
    }
    rect(30, y+40, 30, 30);


    fill(255);
    text("Art-Net subnet " + subnet + " universe " + universe, 70, y+20);
  }
}

Finally, we need a master sketch that can poll and update these parameters. I’ve written one using the library G4P. This sketch can be found on my Github, along with install and build instructions.

OSC control sketch layout

The sketch is quite simple. In the upper left corner, you enter the IP address of the device running the Art-Net to DMX sketch. Clicking “Status” will send the /status command to that IP (port 9009). When a reply is received, the red square will turn green. It will also tell you how many DMX dongles are connected to the sketch. 

With the three text boxes next to the “Push values” button, you can assign an universe and subnet to a device (if only one device is connected, leave “Device” at 0). 

It’s a bit sloppily programmed with no data validity checking, but it’s designed for a sage end-user (you), not the general public. 

Leave Comment

Your email address will not be published. Required fields are marked *