Skip to content

DMX mapping to LED strips

How to map RGB data from an sACN or Art-Net input to one or more physical LED strips in firmware 0.12.11 using a native Berry helper.

DMX.map() is a native C++ helper available from Berry Script. It maps a concrete DMX byte buffer to one or more physical LED ports.

Use it when a controller receives RGB channels from an sACN or Art-Net universe and needs to render them quickly to a physical LED strip or several strips. The conversion runs in the C++ firmware layer, so Berry Script does not need to run a per-pixel for loop.

It is typically called from the render callback of an input SACN or Art-Net IO. This callback receives the latest DMX buffer and can map it during the render phase into PIX1, PIX2 or other port buffers. Physical ports are then displayed through the standard firmware path.

Be careful not to confuse IO label and IO type. The label in io[...] is label_t, so it can have at most 5 characters. For Art-Net, use a short label such as ARTN and set "type": "ARTNET" in the configuration.

You do not need your own ready flag or render callbacks on the physical ports. The important part is that mapping is explicitly connected to the input DMX buffer:

io["SACN"].render = def(channels, offset, frames_received, bytes_received)
DMX.map(channels, 120, 255, io["PIX1"], 340, true, io["PIX2"], 340, false)
end

Typical use:

  • one sACN or Art-Net universe is stretched to one physical LED strip
  • one sACN or Art-Net universe is split into two luminaire branches
  • one sACN or Art-Net universe is split into four separate LED outputs
  • a smaller number of logical RGB colours is smoothly interpolated to more physical pixels
DMX.map(channels, [logical_pixels], [fade], port, [physical_pixels], [reverse], ...)

The same signature written with named arguments:

DMX.map(
channels, # bytes, received DMX RGB buffer
logical_pixels?, # int, default = all RGB pixels in the buffer
fade?, # int 0..255, default = 255
port,
physical_pixels?, # int, default = available port size
reverse?, # bool, default = false
...
) -> bool

Mental model for one call:

map(
channels: bytes,
logical_pixels?: number,
fade?: number,
...targets: {
port: PORT,
physical_pixels?: number,
reverse?: boolean
}[]
): boolean

The parameters are intentionally positional:

  • channels is the bytes buffer passed to io["SACN"].render or io["ARTN"].render.
  • logical_pixels is the optional number of RGB pixels in the input. If omitted, the full available RGB buffer is used. If provided, the input buffer must contain exactly logical_pixels * 3 RGB bytes.
  • fade is the optional interpolation strength between neighbouring logical pixels. 255 means smooth transition; 0 keeps the nearest logical colour without blending.
  • port is the physical output PORT, for example io["PIX1"].
  • physical_pixels is the optional number of physical pixels to write to that port. If missing or 0, the available port size is used.
  • reverse is an optional boolean. true writes physical pixels in the opposite direction.

Each additional port starts another output branch. The source RGB range is split evenly between the listed ports.

Example: 120 RGB values from sACN are smoothly stretched to 680 physical pixels on PIX1.

io["SACN"].render = def(channels, offset, frames_received, bytes_received)
DMX.map(channels, 120, 255, io["PIX1"], 680)
end

For Art-Net, the shape is the same; only the callback is attached to the Art-Net IO. This example uses the short label ARTN, which fits the maximum 5-character limit:

io["ARTN"].render = def(channels, offset, frames_received, bytes_received)
DMX.map(channels, 120, 255, io["PIX1"], 680)
end

Example: the first half of the RGB input goes to PIX1, the second half goes to PIX2. PIX1 is physically wired in reverse, so it uses reverse = true.

io["ARTN"].render = def(channels, offset, frames_received, bytes_received)
DMX.map(
channels,
120, 255,
io["PIX1"], 340, true,
io["PIX2"], 340, false
)
end

Example: 120 logical RGB pixels are split into four physical outputs, 30 logical pixels each. Each output can define its own physical length and direction.

io["SACN"].render = def(channels, offset, frames_received, bytes_received)
DMX.map(
channels,
120, 255,
io["PIX1"], 170, false,
io["PIX2"], 170, false,
io["PIX3"], 170, true,
io["PIX4"], 170, true
)
end

For performance smoke tests, measure the actual received DMX frames and bytes, not the number of Berry render callbacks. In firmware 0.12.11, the callback also receives the number of frames and bytes received since the previous call.

io["SACN"].cb = def(channels, offset, frames_received, bytes_received)
print("received frames", frames_received, "bytes", bytes_received)
end

The Art-Net callback receives the same four arguments:

io["ARTN"].cb = def(channels, offset, frames_received, bytes_received)
print("received frames", frames_received, "bytes", bytes_received)
end

Named callback signature:

io["SACN"].cb = def(
channels, # byte buffer with received DMX slots
offset, # first DMX channel represented by the buffer
frames_received, # number of received sACN frames since the previous callback
bytes_received # number of received DMX data bytes since the previous callback
)
end

DMX.map() only writes colours into the pixel buffers of the individual ports. The physical send to the LED strip is still handled by the standard firmware port render/show step.

io["SACN"].render and io["ARTN"].render are called before the implicit render of physical ports. Use them to fill the port buffers and let the ports display through their normal path.

The callback is called even without a TNGL segment. This is useful for installations where animation frames are produced by an external DMX console and the controller only receives, maps and displays them.

The current implementation is intended for RGB input data. If you receive a DMX order other than RGB, set the input IO to RGB or convert the data to a packed RGB buffer before calling DMX.map().

One call can use up to eight output ports.