Skip to content

Fix Art-Net DMX channel limitation: support full 512 channels per universe#1983

Open
Copilot wants to merge 7 commits intomasterfrom
copilot/fix-artnet-channel-limit
Open

Fix Art-Net DMX channel limitation: support full 512 channels per universe#1983
Copilot wants to merge 7 commits intomasterfrom
copilot/fix-artnet-channel-limit

Conversation

Copy link

Copilot AI commented Feb 18, 2026

Problem

Art-Net device limited to 108 DMX channels (36 RGB LEDs) instead of 512 channels (170 RGB LEDs). Root cause: write() function iterated over _ledRGBCount (byte count = LEDs × 3) while treating loop index as LED count, terminating after 108 bytes.

Buggy code:

for (unsigned int ledIdx = 0; ledIdx < _ledRGBCount; ledIdx++)  // _ledRGBCount = 108 for 36 LEDs
{
    artnet_packet.Data[dmxIdx++] = rawdata[ledIdx];
    if ((ledIdx % 3 == 2) && (ledIdx > 0))
        dmxIdx += (_artnet_channelsPerFixture - 3);
    // Loop ends at ledIdx=107, leaving channels 109-512 uninitialized
}

Changes

Core fix

  • Iterate over LED count, not byte count: for (ledIdx = 0; ledIdx < ledValues.size(); ledIdx++)
  • Explicit RGB channel access: rawdata[ledIdx * 3 + 0/1/2] instead of sequential byte iteration

Buffer safety

  • Check packet capacity before writing: if (dmxIdx + _artnet_channelsPerFixture > DMX_MAX)
  • Send final packet after loop completion (was conditional on loop iteration)

Error handling

  • Fixed return value: if (writeBytes(...) < 0) retVal = -1
  • Previously: retVal &= writeBytes(...) with retVal = 0 always returned 0

Validation

  • Runtime check: _artnet_channelsPerFixture >= 3
  • Schema constraint: "minimum": 3 for channelsPerFixture

Fixed code:

for (unsigned int ledIdx = 0; ledIdx < static_cast<unsigned int>(ledValues.size()); ledIdx++)
{
    if (dmxIdx + _artnet_channelsPerFixture > DMX_MAX)
    {
        // Send packet before overflow
        prepare(thisUniverse, _artnet_seq, dmxIdx);
        if (writeBytes(18 + dmxIdx, artnet_packet.raw) < 0)
            retVal = -1;
        memset(artnet_packet.raw, 0, sizeof(artnet_packet.raw));
        thisUniverse++;
        dmxIdx = 0;
    }
    
    artnet_packet.Data[dmxIdx++] = rawdata[ledIdx * 3 + 0]; // Red
    artnet_packet.Data[dmxIdx++] = rawdata[ledIdx * 3 + 1]; // Green
    artnet_packet.Data[dmxIdx++] = rawdata[ledIdx * 3 + 2]; // Blue
    dmxIdx += (_artnet_channelsPerFixture - 3);
}

Impact

Enables full 512-channel DMX transmission per universe, supporting ~170 RGB LEDs vs. previous 36 LED limitation.

Original prompt

Bug Description

There is a critical bug in the Art-Net LED device implementation that limits transmission to only 108 DMX channels (36 RGB LEDs) instead of the full 512 channels (170 RGB LEDs) per universe.

Symptoms

  1. Channel limitation: Only channels 1-108 are transmitted correctly
  2. Unstable values: Channels 109-512 show random/constantly changing DMX values when using Art-Net
  3. Expected behavior: Should support up to 512 channels (approximately 170 RGB devices) per universe

Root Cause

The bug is in libsrc/leddevice/dev_net/LedDeviceUdpArtNet.cpp in the write() function (lines 103-122).

The loop incorrectly iterates over _ledRGBCount (total number of RGB bytes: LEDs × 3) while treating ledIdx as if it counts individual LEDs. This causes the loop to terminate after processing exactly 108 bytes (36 LEDs × 3 colors).

Current buggy code:

for (unsigned int ledIdx = 0; ledIdx < _ledRGBCount; ledIdx++)
{
    artnet_packet.Data[dmxIdx++] = rawdata[ledIdx];
    if ( (ledIdx % 3 == 2) && (ledIdx > 0) )
    {
        dmxIdx += (_artnet_channelsPerFixture-3);
    }

    if ( (ledIdx == _ledRGBCount-1) || (dmxIdx >= DMX_MAX) )
    {
        prepare(thisUniverse, _artnet_seq, dmxIdx);
        retVal &= writeBytes(18 + qMin(dmxIdx, DMX_MAX), artnet_packet.raw);

        memset(artnet_packet.raw, 0, sizeof(artnet_packet.raw));
        thisUniverse ++;
        dmxIdx = 0;
    }
}

Example: With 36 LEDs:

  • _ledRGBCount = 108 (36 LEDs × 3 bytes)
  • Loop runs: ledIdx = 0 to 107
  • At ledIdx = 107, the condition (107 % 3 == 2) is true
  • After processing byte 107 and adding channel spacing, the loop ends because it reaches _ledRGBCount
  • Channels 109-512 are never written, containing garbage/uninitialized data

Additional Issues Found

  1. Return value bug (line 116): retVal &= writeBytes(...) with retVal initialized to 0 will always return 0 (failure) even on success
  2. Incorrect size calculation (line 116): Using qMin(dmxIdx, DMX_MAX) is unnecessary and incorrect since the condition already prevents dmxIdx from exceeding DMX_MAX

Required Fix

Replace the write() function in libsrc/leddevice/dev_net/LedDeviceUdpArtNet.cpp with the corrected version that:

  1. Iterates over LEDs (not RGB bytes): Loop over ledValues.size() instead of _ledRGBCount
  2. Explicitly writes R, G, B for each LED: Access rawdata[ledIdx * 3 + 0/1/2]
  3. Fixes return value handling: Initialize retVal properly and accumulate errors correctly
  4. Removes unnecessary qMin(): Use dmxIdx directly in the size calculation

Corrected implementation:

int LedDeviceUdpArtNet::write(const QVector<ColorRgb> &ledValues)
{
	int retVal = 0;
	int thisUniverse = _artnet_universe;
	auto rawdata = reinterpret_cast<const uint8_t *>(ledValues.data());

	/*
	This field is incremented in the range 0x01 to 0xff to allow the receiving node to resequence packets.
	The Sequence field is set to 0x00 to disable this feature.
	*/
	if (_artnet_seq++ == 0)
	{
		_artnet_seq = 1;
	}

	int dmxIdx = 0; // offset into the current dmx packet

	memset(artnet_packet.raw, 0, sizeof(artnet_packet.raw));
	
	// Iterate over LEDs, not RGB bytes
	for (unsigned int ledIdx = 0; ledIdx < static_cast<unsigned int>(ledValues.size()); ledIdx++)
	{
		// Write R, G, B for this LED
		artnet_packet.Data[dmxIdx++] = rawdata[ledIdx * 3 + 0]; // Red
		artnet_packet.Data[dmxIdx++] = rawdata[ledIdx * 3 + 1]; // Green
		artnet_packet.Data[dmxIdx++] = rawdata[ledIdx * 3 + 2]; // Blue
		
		// Skip extra channels if fixture needs more than RGB
		dmxIdx += (_artnet_channelsPerFixture - 3);

		// is this the last LED of last packet || last LED that fits in current packet
		if ( (ledIdx == static_cast<unsigned int>(ledValues.size()) - 1) || (dmxIdx >= DMX_MAX) )
		{
			prepare(thisUniverse, _artnet_seq, dmxIdx);
			
			if (writeBytes(18 + dmxIdx, artnet_packet.raw) < 0)
			{
				retVal = -1;
			}

			memset(artnet_packet.raw, 0, sizeof(artnet_packet.raw));
			thisUniverse++;
			dmxIdx = 0;
		}
	}

	return retVal;
}

Testing

This fix has been validated against the reported issue where a Swisson XMT-500 device (supporting both Art-Net and sACN) only received valid data on channels 1-108, with channels 109-512 showing unstable/random values.

After the fix, all 512 channels per universe should work correctly, supporting up to ~170 RGB LEDs per universe.

Files to Modify

  • libsrc/leddevice/dev_net/LedDeviceUdpArtNet.cpp - Replace the write() function (lines 85-125)

This pull request was created from Copilot chat.


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@hyperion-project
Copy link

Hello @copilot 👋

I'm the Hyperion Project Bot and I want to thank you for
contributing to Hyperion with your pull requests!

To help you and other users test your pull requests faster,
I'll create a link for you to your workflow artifacts.

🔗 https://github.com/hyperion-project/hyperion.ng/actions/runs/22155635218

Of course, if you make changes to your PR, I will create a new link.

Best regards,
Hyperion Project

@Lord-Grey Lord-Grey marked this pull request as ready for review February 18, 2026 20:06
- Changed loop to iterate over LEDs (ledValues.size()) instead of RGB bytes (_ledRGBCount)
- Explicitly write R, G, B for each LED using rawdata[ledIdx * 3 + 0/1/2]
- Fixed return value handling to properly report errors
- Removed unnecessary qMin() in size calculation
- This fixes the limitation that prevented transmission beyond 108 DMX channels (36 RGB LEDs)

Co-authored-by: Lord-Grey <48840279+Lord-Grey@users.noreply.github.com>
@hyperion-project
Copy link

Hey @copilot I created a new link to your workflow artifacts:
🔗 https://github.com/hyperion-project/hyperion.ng/actions/runs/22155710172

- Check buffer boundaries before writing to prevent overflow
- Move packet boundary check before LED data write
- Add validation for channelsPerFixture (minimum 3)
- Add minimum constraint to schema
- Send final packet after loop completes

Co-authored-by: Lord-Grey <48840279+Lord-Grey@users.noreply.github.com>
@hyperion-project
Copy link

Hey @copilot I created a new link to your workflow artifacts:
🔗 https://github.com/hyperion-project/hyperion.ng/actions/runs/22155776739

Co-authored-by: Lord-Grey <48840279+Lord-Grey@users.noreply.github.com>
@hyperion-project
Copy link

Hey @copilot I created a new link to your workflow artifacts:
🔗 https://github.com/hyperion-project/hyperion.ng/actions/runs/22155796478

Copilot AI changed the title [WIP] Fix Art-Net implementation to support 512 DMX channels Fix Art-Net DMX channel limitation: support full 512 channels per universe Feb 18, 2026
Copilot AI requested a review from Lord-Grey February 18, 2026 20:11
@hyperion-project
Copy link

Hey @copilot I created a new link to your workflow artifacts:
🔗 https://github.com/hyperion-project/hyperion.ng/actions/runs/22239665784

@hyperion-project
Copy link

Hey @Copilot I created a new link to your workflow artifacts:
🔗 https://github.com/hyperion-project/hyperion.ng/actions/runs/22240455249

Lord-Grey added a commit that referenced this pull request Mar 6, 2026
@hyperion-project
Copy link

Hey @copilot I created a new link to your workflow artifacts:
🔗 https://github.com/hyperion-project/hyperion.ng/actions/runs/22773721041

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants