GitHub - HakanL/Haukcode.sACN: Simple sACN library for .NET (standard) (original) (raw)

A high-performance, cross-platform sACN (E1.31) library for .NET

Table of Contents

What is sACN (E1.31)?

sACN (Streaming ACN) is a network protocol standardized as ANSI E1.31, designed for efficiently transmitting DMX512 lighting control data over IP networks using multicast or unicast UDP packets. It's widely used in entertainment lighting, architectural lighting, and stage production.

Key Concepts:

Official Specification: ANSI E1.31-2018

Features

Full sACN/E1.31 Support

High Performance

Cross-Platform

Easy to Use

Installation

Install via NuGet Package Manager:

dotnet add package Haukcode.sACN

Or via Package Manager Console:

Install-Package Haukcode.sACN

Or add directly to your .csproj file:

Quick Start

Receiving DMX Data

using Haukcode.sACN; using Haukcode.sACN.Model; using System; using System.Net; using System.Threading.Channels;

// Create a unique sender ID and name for this client var senderId = Guid.NewGuid(); var senderName = "MyApp";

// Channel for receiving packets var channel = Channel.CreateUnbounded();

// Create receive client var client = new SACNClient( senderId: senderId, senderName: senderName, localAddress: IPAddress.Any, channelWriter: p => WritePacketAsync(channel, p), channelWriterComplete: () => channel.Writer.Complete());

// Join Universe 1 to start receiving data client.JoinDMXUniverse(1);

// Process incoming packets await foreach (var packet in channel.Reader.ReadAllAsync()) { var dataLayer = packet.Packet.RootLayer.FramingLayer as DataFramingLayer; if (dataLayer != null) { Console.WriteLine($"Universe {dataLayer.UniverseId}: {dataLayer.DMPLayer.Data.Length} channels"); } }

async Task WritePacketAsync(Channel ch, ReceiveDataPacket pkt) { await ch.Writer.WriteAsync(pkt); }

Sending DMX Data

using Haukcode.sACN; using System.Net;

var senderId = Guid.NewGuid(); var senderName = "MyDMXController";

// Get local network interface var localAddress = Haukcode.Network.Utils.GetFirstBindAddress().IPAddress;

// Create send client var client = new SACNClient( senderId: senderId, senderName: senderName, localAddress: localAddress);

// Prepare DMX data (512 channels, values 0-255) byte[] dmxData = new byte[512]; dmxData[0] = 255; // Channel 1 at full dmxData[1] = 128; // Channel 2 at 50%

// Send to Universe 1 (multicast) await client.SendDmxData( address: null, // null = multicast universeId: 1, dmxData: dmxData, priority: 100);

// Send to specific device (unicast) await client.SendDmxData( address: IPAddress.Parse("192.168.1.100"), universeId: 1, dmxData: dmxData, priority: 100);

Usage Examples

Receiving DMX Data

Here's a complete example of receiving and processing DMX data from multiple universes:

using Haukcode.sACN; using Haukcode.sACN.Model; using System.Net; using System.Threading.Channels;

public class DMXReceiver { private SACNClient client; private Channel channel;

public async Task StartAsync()
{
    channel = Channel.CreateUnbounded<ReceiveDataPacket>();
    
    client = new SACNClient(
        senderId: Guid.NewGuid(),
        senderName: "DMX Receiver",
        localAddress: IPAddress.Any,
        channelWriter: async p => await channel.Writer.WriteAsync(p),
        channelWriterComplete: () => channel.Writer.Complete());

    // Subscribe to error events
    client.OnError.Subscribe(error =>
    {
        Console.WriteLine($"Error: {error.Message}");
    });

    // Join multiple universes
    client.JoinDMXUniverse(1);
    client.JoinDMXUniverse(2);
    client.JoinDMXUniverse(3);

    // Process packets
    await ProcessPacketsAsync();
}

private async Task ProcessPacketsAsync()
{
    await foreach (var packet in channel.Reader.ReadAllAsync())
    {
        var dataLayer = packet.Packet.RootLayer.FramingLayer as DataFramingLayer;
        if (dataLayer != null)
        {
            var dmpLayer = dataLayer.DMPLayer;
            
            // Only process start code 0 (standard DMX)
            if (dmpLayer.StartCode == 0)
            {
                Console.WriteLine($"Source: {dataLayer.SourceName}");
                Console.WriteLine($"Universe: {dataLayer.UniverseId}");
                Console.WriteLine($"Sequence: {dataLayer.SequenceId}");
                Console.WriteLine($"Priority: {dataLayer.Priority}");
                Console.WriteLine($"Channels: {dmpLayer.Data.Length}");
                
                // Access DMX channel values
                var channelValues = dmpLayer.Data.ToArray();
                Console.WriteLine($"Channel 1: {channelValues[0]}");
            }
        }
    }
}

public void Stop()
{
    // Clean up
    client.DropAllInputUniverses();
    client.Dispose();
}

}

Sending DMX Data

Complete example showing how to send DMX data with various options:

using Haukcode.sACN; using System.Net;

public class DMXSender { private SACNClient client; private byte[] dmxBuffer = new byte[512];

public void Initialize()
{
    var localIP = Haukcode.Network.Utils.GetFirstBindAddress().IPAddress;
    
    client = new SACNClient(
        senderId: Guid.NewGuid(),
        senderName: "My Lighting Controller",
        localAddress: localIP);
}

public async Task SendLightingDataAsync()
{
    // Set some channel values
    dmxBuffer[0] = 255;   // Channel 1 - Dimmer at full
    dmxBuffer[1] = 200;   // Channel 2 - Red at 78%
    dmxBuffer[2] = 150;   // Channel 3 - Green at 59%
    dmxBuffer[3] = 100;   // Channel 4 - Blue at 39%

    // Send via multicast (standard sACN)
    await client.SendDmxData(
        address: null,
        universeId: 1,
        dmxData: dmxBuffer,
        priority: 100);
}

public async Task SendToSpecificDevice()
{
    // Send via unicast to a specific device
    await client.SendDmxData(
        address: IPAddress.Parse("192.168.1.50"),
        universeId: 1,
        dmxData: dmxBuffer,
        priority: 100);
}

public async Task TerminateStream()
{
    // Send termination packet
    await client.SendDmxData(
        address: null,
        universeId: 1,
        dmxData: Array.Empty<byte>(),
        priority: 100,
        terminate: true);
}

public void Dispose()
{
    client?.Dispose();
}

}

Multicast vs Unicast

Multicast (pass null as address):

// Multicast to all listeners on Universe 1 await client.SendDmxData(null, 1, dmxData);

Unicast (pass specific IP address):

// Unicast to specific device await client.SendDmxData(IPAddress.Parse("192.168.1.100"), 1, dmxData);

Sync Packets

Synchronization packets ensure multiple universes update simultaneously, essential for effects spanning multiple fixtures:

// Send data to multiple universes with sync ushort syncUniverse = 7000; // Sync universe ID

// Send data with sync flag await client.SendDmxData(null, 1, dmxData1, syncAddress: syncUniverse); await client.SendDmxData(null, 2, dmxData2, syncAddress: syncUniverse); await client.SendDmxData(null, 3, dmxData3, syncAddress: syncUniverse);

// Trigger synchronized update await client.SendSync(null, syncUniverse);

When to use sync packets:

Trigger Universes

Listen for sync packets on specific universes without receiving the full DMX data:

// Join universe as trigger listener only client.JoinDMXUniverseForTrigger(7000);

// This will receive sync packets but not full DMX data // Useful for timing/synchronization without processing full data streams

API Reference

SACNClient Constructor

public SACNClient( Guid senderId, // Unique identifier for this source string senderName, // Human-readable source name (max 64 chars) IPAddress localAddress, // Local network interface to bind to Func<ReceiveDataPacket, Task>? channelWriter, // Optional callback for received packets Action? channelWriterComplete, // Optional callback when receiving completes int port = 5568) // sACN port (default 5568)

Sending Methods

// Send DMX data Task SendDmxData( IPAddress? address, // null for multicast, IP for unicast ushort universeId, // Universe ID (1-63999) ReadOnlyMemory dmxData, // Up to 512 bytes of DMX data byte priority = 100, // Priority (0-200, default 100) ushort syncAddress = 0, // Sync universe (0 = no sync) byte startCode = 0, // Start code (0 = standard DMX) bool important = false, // Priority queue flag bool terminate = false) // Stream termination flag

// Send sync packet Task SendSync( IPAddress? address, // null for multicast, IP for unicast ushort syncAddress) // Sync universe ID

Receiving Methods

// Join universe to receive full DMX data void JoinDMXUniverse(ushort universeId)

// Drop universe subscription void DropDMXUniverse(ushort universeId)

// Drop all universe subscriptions void DropAllInputUniverses()

// Join universe for sync/trigger packets only void JoinDMXUniverseForTrigger(ushort universeId)

// Drop trigger universe void DropDMXUniverseForTrigger(ushort universeId)

// Drop all trigger subscriptions void DropAllTriggerUniverses()

Properties

Guid SenderId { get; } // This client's sender ID string SenderName { get; } // This client's sender name IPEndPoint LocalEndPoint { get; } // Local endpoint int? ActualReceiveBufferSize { get; } // Actual socket buffer size IObservable OnError { get; } // Error notifications

Advanced Features

Error Handling

Subscribe to error events using reactive extensions:

client.OnError.Subscribe(error => { Console.WriteLine($"sACN Error: {error.Message}"); // Handle error appropriately });

Buffer Management

The library uses efficient buffer pooling to minimize allocations. When receiving data, the DMX data references memory from a shared pool. If you need to keep the data beyond the callback scope, copy it:

var dataLayer = packet.Packet.RootLayer.FramingLayer as DataFramingLayer; if (dataLayer != null) { // Copy data if needed beyond this scope byte[] dmxCopy = dataLayer.DMPLayer.Data.ToArray();

// Store or process dmxCopy later

}

Stream Termination

Properly terminate streams when done:

// Send termination packet for a universe await client.SendDmxData( address: null, universeId: 1, dmxData: Array.Empty(), terminate: true);

Multiple Network Interfaces

List and select specific network interfaces:

// Get all available network interfaces var adapters = Haukcode.Network.Utils.GetAllBindAddresses();

foreach (var adapter in adapters) { Console.WriteLine($"{adapter.Name}: {adapter.IPAddress}"); }

// Use specific interface var selectedInterface = adapters.First(a => a.Name.Contains("Ethernet")); var client = new SACNClient( senderId: Guid.NewGuid(), senderName: "MyApp", localAddress: selectedInterface.IPAddress);

Network Configuration

Firewall Settings

Ensure UDP port 5568 is open for sACN communication:

Windows:

New-NetFirewallRule -DisplayName "sACN" -Direction Inbound -Protocol UDP -LocalPort 5568 -Action Allow

Linux (ufw):

Multicast Configuration

sACN uses multicast addresses in the range 239.255.0.0/16:

Ensure your network switches support IGMP snooping for efficient multicast delivery.

Performance Tuning

The library sets reasonable defaults, but you can monitor buffer usage:

Console.WriteLine($"Receive buffer size: {client.ActualReceiveBufferSize} bytes");

For high-traffic scenarios:

Performance Considerations

Troubleshooting

No packets received

  1. Check network interface: Ensure you're binding to the correct interface
    var addresses = Haukcode.Network.Utils.GetAllBindAddresses();
    // Verify the address you're using
  2. Check universe subscription: Verify you've joined the universe
    client.JoinDMXUniverse(1);
  3. Firewall: Ensure UDP port 5568 is allowed
  4. Multicast: Verify multicast is enabled on network switches

Packet loss

  1. Buffer size: Check if receive buffer is sufficient
    Console.WriteLine($"Buffer: {client.ActualReceiveBufferSize}");
  2. Processing speed: Ensure packet processing is fast enough
  3. Network capacity: Check for network congestion

Platform-specific issues

Linux: Ensure proper permissions for multicast:

sudo sysctl -w net.ipv4.igmp_max_memberships=100

Windows: Verify Windows Firewall allows multicast traffic

Credits

License

This project is licensed under the MIT License - see the LICENSE file for details.


Specification Reference: ANSI E1.31-2018 sACN Protocol