LEGO and DMX

This post is part 1 of 6 of  LEGO and DMX

Playing with MIDI, reading a lot, browsing through musical equipments… and DMX cames out.

DMX is like a custom RS-485 network. A master device sends data at 250 kHz to slave devices, each of them listening to a different “channel” or address. Typical DMX devices (“fixtures”) are stage lights but you can find fog machines, laser projectors, bubble machines, relay boards, analog interfaces…

On a studio scenario you can even join MIDI with DMX so you can create a performance combining music and lights or other kind of effects.

Generating a 250 kHz stream of data isn’t difficult, there are FTDI devices that can do that. I could use the same kind of setup I use to generate LEGO Power Functions IR signals to generate DMX signals.

In fact, people are already doing it. There are a few tutorials on how to create a DMX controller for less than $10 but I prefer this one (it costed me a bit more than 10€ but it was really straightforward, just a FTDI485 cable and a XLR 3-pin female connector.

It can be used with QLC+ (an open source DMX controller software) but that’s overkill for a MINDSTORMS EV3 so after a short search I found a python library to use it directly (dmx485).

So you connect your DYI DMX adapter to your EV3:

[1218334.312176] usb 1-1.2: New USB device found, idVendor=0403, idProduct=6001
[1218334.312248] usb 1-1.2: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[1218334.312287] usb 1-1.2: Product: USB-RS485 Cable
[1218334.312322] usb 1-1.2: Manufacturer: FTDI
[1218334.312355] usb 1-1.2: SerialNumber: FT4NMHF6
[1218334.800717] usbcore: registered new interface driver usbserial
[1218334.936770] usbcore: registered new interface driver ftdi_sio
[1218334.937337] usbserial: USB Serial support registered for FTDI USB Serial Device
[1218334.938673] ftdi_sio 1-1.2:1.0: FTDI USB Serial Device converter detected
[1218334.939512] usb 1-1.2: Detected FT232RL
[1218334.956748] usb 1-1.2: FTDI USB Serial Device converter now attached to ttyUSB0

Here it is assigned to ‘/dev/ttyUSB0’

Now we just need to install ‘dmx485’:

pip3 install dmx485

(if you have a fresh ‘ev3dev’ installation you probably need to install ‘pip3’ first)

For testing, the script on ‘dmx485’ homepage is enough but I adapted it for my first python DMX script to control my cheap DMX RGB PAR spotlight (after reading the manual to find how it works – it uses 4 DMX channels for Master, Red, Green and Blue values)

!/usr/bin/env python3
import time
import dmx
import random

random.seed()

CMD = [0]*512
CMD[3] = 255  # Master Dimmer
CMD[4] = 0    # Red Dimmer
CMD[5] = 0    # Green Dimmer
CMD[6] = 0    # Blue Dimmer

BLANK = [0]*512

DELAY = 0.2
sender = dmx.DMX_Serial('/dev/ttyUSB0')
sender.start()

# WHITE
CMD[4] = 255
CMD[5] = 255
CMD[6] = 255
sender.set_data(bytes(CMD))
time.sleep(2)
sender.set_data(bytes(BLANK))

# RED dimmer
CMD[4] = 0
CMD[5] = 0
CMD[6] = 0
for i in range(0, 256, 8):
    CMD[4] = i
    sender.set_data(bytes(CMD))
    time.sleep(DELAY)
sender.set_data(bytes(BLANK))

# GREEN dimmer
CMD[4] = 0
for i in range(0, 256, 8):
    CMD[5] = i
    sender.set_data(bytes(CMD))
    time.sleep(DELAY)
sender.set_data(bytes(BLANK))

# BLUE dimmer
CMD[5] = 0
for i in range(0, 256, 8):
    CMD[6] = i
    sender.set_data(bytes(CMD))
    time.sleep(DELAY)
sender.set_data(bytes(BLANK))

# MASTER dimmer
CMD[4] = 255
CMD[5] = 255
CMD[6] = 255
for i in range(0, 256, 8):
    CMD[3] = i
    sender.set_data(bytes(CMD))
    time.sleep(DELAY)
sender.set_data(bytes(BLANK))

# RGB random
CMD[3] = 255
CMD[4] = 0
CMD[5] = 0
CMD[6] = 0
for step in range(50):
    CMD[4] = random.randint(0,255)
    CMD[5] = random.randint(0,255)
    CMD[6] = random.randint(0,255)
    print(CMD[4], CMD[5], CMD[6])
    sender.set_data(bytes(CMD))
    time.sleep(DELAY)
sender.set_data(bytes(BLANK))

It was amazingly easy.

Now the tough part is using this same USB DMX adapter to listen to DMX signals and make my own LEGO MINDSTORMS DMX fixture. I am not keen to continuously polling the bus so I probably have to spend a few more euros to buy a smarter USB DMX adapter that offloads the reception task.

DMX slave?

This post is part 2 of 6 of  LEGO and DMX

Controlling DMX fixtures with EV3 is cool but what I really want is creating my own LEGO fixtures – like the bubble maker.

The USB DMX adapter is great for generating DMX messages but I was in doubt if it could be also used to receive DMX messages since most DMX interfaces that allow TX/RX make use of some kind of microcontroller to reduce overhead.

Nevertheless I decided to give it a try. The cable uses a FTDI FT232R chip that is supported by PyFtdi for speeds up to 3 Mbps and the DMX “only” requires 250 kbps.

First attempts on my laptop with the very cheap 6-channel controller I have look promising:

import pyftdi.serialext
port = pyftdi.serialext.serial_for_url('ftdi://ftdi:232:FT4NMHF6/1', baudrate=2500000)
port.timeout=0.002
port.reset_input_buffer()
port.read(8)

With channels 1, 3 and 5 at maximum and channels 2, 4 and 6 at minimum I get:

b'\x00\x00\xff\x00\xff\x00\xff\x00'

and with the opposite (channels 1, 3 and 5 at minum and channels 2, 4 and 6 at maximum) I get:

b'\x00\x00\x00\xff\x00\xff\x00\xff'

The second byte always being ‘\x00’ is expected – the first frame or Start Code is ‘0’ for most common DMX usages.

And the first byte is the practical result of “the start-of-packet procedure is a logic zero for more than 22 bit periods,followed by a logic 1 for more than 2 bit periods“.

Now the problem is that if I read more than 8 bytes I will get a the same results repeated – for instance if I read 514 bytes (start-of-packet + start code + 512 channels) with all 6 channels at maximum I will get:

b'\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff'

So is it my cheap 6-channel controller really sending just 6 channel data instead of full 512 channel data (so 6 ‘FF’ followed by 506 ’00’) or am I doing something wrong when reading? Most probably the second option, of course.

The timeout = 0.002 was just an attempt, I probably need to use 0.023 (514 frames at 250 kbps take 22.616 ms).

And I also need a 3 pin XLR male to male adapter to assure connections are stable. But if this works I expect to have a DMX BUBBL3R before Easter.

My first LEGO DMX fixture

This post is part 3 of 6 of  LEGO and DMX

probably also first LEGO DMX fixture ever in the multiverse 🙂

So my Cameo Control 6 DMX controller is not probably full DMX compliant – I can only read 6 channels and even so only when cutting one of the data wires off. But who cares? (I do, but this month’s budget is already exceeded so I will only get a more decent DMX controller for Easter).

But using PyFtdi on EV3 works:

pip3 install ftdi

I could not confirm my FTDI url with ‘ftdi_urls.py’ like I did on my laptop but used same url (‘ftdi://ftdi:232:FT4NMHF6/1’) and it worked:

#!/usr/bin/env python3

from ev3dev2.motor import MediumMotor, LargeMotor, DcMotor, OUTPUT_A, OUTPUT_B, OUTPUT_C,  SpeedPercent
from ev3dev2.port import LegoPort
from time import sleep

lights_port = LegoPort(OUTPUT_C)
lights_port.mode = 'dc-motor'

sleep(1.5)

import pyftdi.serialext
port = pyftdi.serialext.serial_for_url('ftdi://ftdi:232:FT4NMHF6/1', baudrate=250000)
port.timeout=0

port.reset_input_buffer()
port.read(8)

channels = [0]*6

pan = MediumMotor(OUTPUT_A)
pan.position = 0
pan.speed_sp = 90

tilt = LargeMotor(OUTPUT_B)
tilt.position = 0
tilt.speed_sp = 90

lights = DcMotor(OUTPUT_C)
lights.run_direct()
lights.duty_cycle_sp = 0

while True:
    port.reset_input_buffer()
    dmx_data = port.read(8)
    sleep(0.01)
    port.reset_input_buffer()
    port.read(8)

    print(dmx_data)
    i = 0
    error = False
    for b in dmx_data:
    #    print(b)
        if i == 0 or i == 1:
            if b != 0:
                error = True
        else:
            channels[i-2] = b
        i = i + 1
    if error:
        print("Error!")
    else:
#        print("Channels:", channels)
        pan.on_to_position(SpeedPercent(25), channels[0] * 90/255 - 45)
        tilt.on_to_position(SpeedPercent(25), 45 - channels[1] * 90/255)
        lights.duty_cycle_sp = channels[2] * (100/255)


Now it’s time to plan a few DMX fixtures: a bubble machine, a fog machine, a spark machine, a confetti cannon, a flamethrower… 😀

Compatibility issues

This post is part 4 of 6 of  LEGO and DMX

So my second DMX fixture arrived today: a Fonestar PAR-18L.

I had ordered a few weeks ago with a cheap Waveshare USB-RS485 adapter but the order took ages so last week I went for the Ibiza PAR-MINI-RGB-3 and the FTDI USB-RS485-WE-1800-BT cable.

So:

  • My adapter based on the FTDI cable can control both PARs
  • My cheap Cameo Control 6 controller can only control the Ibiza PAR
  • An adapter based on the Waveshare adapter cannot control the Ibiza PAR (sometimes I get a short flash of the intended color)

So my Cameo DMX controller was a bad starter choice, I will need a better controller to confirm my receiver code is working correctly. Will try to get a better one next month, with a few more channels and (if possible) some MIDI integration.

–edited–

Now even more strange: the Waveshare adapter does work… but only with the new Fonestar PAR.

I changed the Ibiza address to 008 and kept Fonestar at 001 the joined both on the DMX chain and changed my python script to use both. With the FTDI cable both PARs work. With the Waveshare only the Fonestar work, no matter if I put it as the first or the last of the chain so it doesn’t seem to be a termination problem.

As the Cameo controller works with the Ibiza but not with the Fonestar AND the Cameo controller data can only be read by my scripts when I cute B (DMX-) wire… I suspect both the Cameo and the Ibiza have A/B (DMX+/DMX-) reversed or even stranger deviations from the DMX standard.

DMX – a few findings

This post is part 5 of 6 of  LEGO and DMX

Installed QLC+ on my laptop. Great tool, giving up on bying a DMX controller.

QLC+ recognizes my FTDI cable adapter (and my Waveshare adapter):

Plugin   Device                            Input   Output  Feedback
 DMX USB  1:USB-RS485 Cable (S/N:FT4NMHF6)  -       X       -
 DMX USB
 This plugin provides DMX output support for DMXKing ultraDMX range, Enttec DMX USB Pro, Enttec Open DMX USB, FTDI USB COM485 Plus1, Vince USB-DMX512 and compatible devices.

No input support available.
1: USB-RS485 Cable (S/N: FT4NMHF6)
Device is operating correctly.
Driver in use: libFTDI
Protocol: Open DMX USB
Manufacturer: FTDI
DMX Channels: 512
DMX Frame Frequency: 30Hz
System Timer Accuracy: Patch this widget to a universe to find out

In the plugin settings I can change Mode. Open TX (above) allows sending DMX messages, Open RX allows receiving. And it works.

Using the two USB adapters – one on my laptop and the other on EV3 – I used QLC+ to test my scripts on EV3:

  • using QLC+ as controller, my EV3 python scripts (pyftdi-based) can decode messages on any of the 512 channels… as long as I lower DMX frame frequency to 9 Hz.
  • using the Ev3 as controller (python scripts using dmx485) I can receive messages on all 512 channels on QLC+

but…

  • using laptop as a controller (python dxm485 instead of QLC+) the same pyftdi-based scripts on EV3 no longer work… unless I force sender object to stop immediately after the first frame:
sender.start()
 while True:
    sender.start()
    sender.set_data(bytes(CMD))
    sleep(0.115)
    sender.stop()
    sleep(1.0)

It seems dmx485 forces a DMX Frame Frequency too high for my decoding scripts to keep up – it seems to be a limitation of EV3 because reverting the scenario (EV3 as a controller with dmx485-based script and laptop as a receiver with pyftdi-based script) it works.

Good that QLC+ has an option to adjust it – 9 Hz isn’t good for light animations but is more than enough for what I want. Will use it as a DMX controller from now on.

Interesting, QLC+ also has a MIDI plugin. It can listen to MIDI channels and take actions… like activating a DMX fixture when a selected note is played.

So… MIDI brings me to DMX… DMX brings me back to MIDI. Such a small world.

LEGO DMX Fire Machine

This post is part 6 of 6 of  LEGO and DMX

Second LEGO DMX fixture: a Fire Machine 🙂
Similar script on EV3, listening to DMX channel 1 and reacting to just two values:

  • 255 turns the BIC lighter ON
  • 0 turns it OFF again

Working quite good with QLC+ as a DMX controller (at 9 Hz DMX frame rate) and other DMX fixtures on the loop (in this video a Fonestar PAR-18L set to DMX channel 2):

‘DMXfire.py’ script:

#!/usr/bin/env python3

from ev3dev2.motor import MediumMotor, OUTPUT_A, SpeedPercent
from time import sleep
import pyftdi.serialext

port = pyftdi.serialext.serial_for_url('ftdi://ftdi:232:AG0KAEQB/1', baudrate=250000)

#works good with QLC+ at 9 Hz
port.timeout = 0.0226

# clear input buffer (not sure if everything needed)
port.reset_input_buffer()
port.read(514)
port.reset_input_buffer()

channels = [0]*512
CH_FIRE = 1

fire = MediumMotor(OUTPUT_A)
fire.reset()
fire.position_sp = 0
fire.speed_sp = 1560

fireON = False

while True:
    port.reset_input_buffer()
    dmx_data = port.read(514)

    i = 0
    error = False
    for b in dmx_data:
        if i == 0 or i == 1:
            if b != 0:
                error = True
        else:
            channels[i-2] = b
        i = i + 1
    if error:
        # first 2 bytes should be zero ("start-of-packet procedure" and "start frame") 
        print("Error!")
    else:   
        if channels[CH_FIRE -1] == 255:
            if fireON == False:
                # turn Fire ON
                print('Fire triggered ON')
                fireON = True
                fire.position_sp = 3050
                fire.run_to_abs_pos()
        elif channels[CH_FIRE -1] == 0:
            if fireON == True:
            # turn Fire OFF 
            print('Fire triggered OFF')
            fireON = False 
            fire.position_sp = 0
            fire.run_to_abs_pos() 
    print("FireON: ", fireON)
    sleep(0.1)