DMX lighting control

DMX lighting control with LogicMachine #

DMX user library #

Go to Scripting > User libraries and create a library named dmx. Copy/paste the following code into the library.

local luadmx = require('luadmx')
module('DMX', package.seeall)

local DMX = {}

-- default params
local defaults = {
  -- storage key
  skey = 'dmx_line_1',
  -- RS-485 port
  port = '/dev/RS485-1',
  -- number of calls per second
  resolution = 20,
  -- total number of channels to use
  channels = 3,
  -- transition time in seconds, does not include DMX transfer time
  transition = 2,
}

-- value setter
function set(chan, val, key)
  key = key or defaults.skey
  chan = tonumber(chan) or 0
  val = tonumber(val) or -1

  -- validate channel number and value
  if chan >= 1 and chan <= 512 and val >= 0 and val <= 255 then
    storage.exec('lset', key, chan - 1, val)
  end
end

-- value getter
function get(chan, key)
  local res, val
  key = key or defaults.skey
  chan = tonumber(chan) or 0

  -- validate channel number and value
  if chan >= 1 and chan <= 512 then
    res = storage.exec('lrange', key, chan - 1, chan - 1)
    if type(res) == 'table' then
      val = tonumber(res[ 1 ])
    end
  end

  return val
end

-- DMX init, returns new DMX object
function init(params)
  local n, k, v, _

  -- create metatable and set user parameters
  n = setmetatable({}, { __index = DMX })
  n.params = params or {}

  _, n.conn = pcall(require('redis').connect)

  -- merge parameters that are set by user
  for k, v in pairs(defaults) do
    if n.params[ k ] == nil then
      n.params[ k ] = v
    end
  end

  n:reset()

  return n
end

function DMX:reset()
  local err, chan, params

  params = self.params
  self.dm, err = luadmx.open(params.port)

  -- error while opening
  if err then
    os.sleep(1)
    error(err)
  end

  -- set channel count
  self.dm:setcount(params.channels)

  -- number of transaction ticks
  self.ticks = math.max(1, params.transition * params.resolution)

  -- calculate sleep time
  self.sleep = 1 / params.resolution

  -- reset channel map
  self.channels = {}

  -- empty channel value map
  self.conn:ltrim(params.skey, 1, 0)

  -- fill channel map
  for chan = 1, params.channels do
    self.channels[ chan ] = { current = 0, target = 0, ticks = 0 }

    -- turn off by default
    self.conn:lpush(params.skey, 0)
    self.dm:setchannel(chan, 0)
  end
end

-- get new values
function DMX:getvalues()
  local max, channels, ticks, values, val

  max = self.params.channels
  channels = self.channels
  ticks = self.ticks
  values = self.conn:lrange(self.params.skey, 0, max - 1) or {}

  -- check for new values for each channel
  for chan = 1, max do
    val = tonumber(values[ chan ]) or 0

    -- target value differs, set transcation
    if val ~= channels[ chan ].target then
      channels[ chan ].target = val
      channels[ chan ].delta = (channels[ chan ].target - channels[ chan ].current) / ticks
      channels[ chan ].ticks = ticks
    end
  end
end

-- main loop handler
function DMX:run()
  self:getvalues()

  -- transition loop
  for i = 1, self.params.resolution do
    self:step()
    self.dm:send()
    os.sleep(self.sleep)
  end
end

-- single transition step
function DMX:step()
  local chan, channels, t

  channels = self.channels

  -- transition for each channel
  for chan = 1, self.params.channels do
    t = channels[ chan ].ticks

    -- transition is active
    if t > 0 then
      t = t - 1

      channels[ chan ].current = channels[ chan ].target - channels[ chan ].delta * t
      channels[ chan ].ticks = t

      self.dm:setchannel(chan, channels[ chan ].current)
    end
  end
end

DMX handler script #

Add the following resident script with sleep interval = 0, adjust port and channel count as needed.

if not dmxhandler then
  require('user.dmx')
  dmxhandler = DMX.init({
    port = '/dev/RS485-1', -- RS-485 port name
    channels = 8, -- number of DMX channels to use
    transition = 2, -- soft transition time in seconds
  })
end

dmxhandler:run()

Single object example #

The following event script example sets DMX channel 1 to a supplied 1 byte scaling value (0..100).

channel = 1
value = event.getvalue()
value = math.round(value * 2.55)
require('user.dmx')
DMX.set(channel, value)

Multiple object example #

Create objects with a DMX tag, where the last part of the group address is the DMX address (starting from 1). Create event script mapped to DMX tag. All objects must have 1 byte scaling data type set.

require('user.dmx')
-- get ID as group address last part (x/y/ID)
id = tonumber(event.dst:split('/')[3])
-- get event value (1 byte scaling)
value = event.getvalue()
-- convert from [0..100] to [0..255]
value = math.round(value * 2.55)
-- set channel ID value
DMX.set(id, value)

Predefined scene example #

The following example should be placed inside a resident script. Sleep time defines scene keep time (at least 1 second).

scenes table contains values for 3 channels in 0..255 range.

if not scenes then
  require('user.dmx')

  -- 3 channel scene
  scenes = {
    { 255, 0, 0 },
    { 0, 255, 0 },
    { 0, 0, 255 },
    { 255, 255, 0 },
    { 0, 255, 255 },
    { 255, 0, 255 },
    { 255, 255, 255 },
  }

  current = 1
end

-- set current scene values
scene = scenes[ current ]
for i, v in ipairs(scene) do
  DMX.set(i, v)
end

-- switch to next scene
current = current + 1
if current > #scenes then
  current = 1
end

Random scene example #

The following example should be placed inside a resident script. Sleep time defines scene keep time (at least 1 second).

require('user.dmx')

-- number of steps to use, e.g. 3 steps = { 0, 127, 255 }
steps = 5
-- number of channels to set
channels = 3
-- first channel number
offset = 1

for i = offset, channels do
  v = math.random(0, (steps - 1)) * 255 / (steps - 1)
  DMX.set(i, math.floor(v))
end