Chapter 215: sACN (E1.31) Streaming ACN Protocol

Chapter Objectives

By the end of this chapter, you will be able to:

  • Explain the purpose of sACN (E1.31) as an ANSI standard for DMX over IP.
  • Differentiate between sACN and Art-Net, particularly regarding networking models.
  • Understand and use IP multicast for efficient data distribution.
  • Describe the layered structure of an sACN packet.
  • Write C code to receive, parse, and handle sACN data, including network byte order conversion.
  • Build a functional sACN to DMX gateway using an ESP32.

Introduction

In the last chapter, we built an Art-Net node, transforming our ESP32 into a gateway between a Wi-Fi network and a physical DMX chain. Art-Net is a ubiquitous de-facto standard, but the professional lighting and entertainment industry also relies heavily on an official ANSI (American National Standards Institute) standard: E1.31, commonly known as sACN (Streaming Architecture for Control Networks).

While both protocols solve the same problem—transporting DMX over IP—they do so with different philosophies and technical approaches. sACN is designed from the ground up for scalability, reliability, and interoperability in large, complex networks. It introduces features like prioritized streams and, most notably, leverages IP multicast for highly efficient network communication.

Understanding sACN is essential for anyone looking to work in professional lighting environments. In this chapter, we will adapt our previous project to create an sACN-compliant node, diving deep into the multicast networking and byte-order-aware parsing that sets this protocol apart.

Theory

1. sACN vs. Art-Net: A Tale of Two Standards

While Art-Net often uses broadcast or unicast, sACN is built almost exclusively around IP multicast.

  • Broadcast (Art-Net): Packets are sent to every single device on the local network, whether they are interested or not. This is simple but can create unnecessary network traffic, like shouting in a crowded room.
  • Unicast (Art-Net): Packets are sent directly from one controller to one node’s IP address. This is efficient but requires the controller to know the IP of every node.
  • Multicast (sACN): This is the elegant middle ground. A controller sends a packet to a special multicast group address. Nodes can choose to “subscribe” to this group. The network hardware (routers and switches) is smart enough to only forward the packets to devices that have subscribed. It’s like a club where you only get the newsletter if you’ve signed up for it.

2. Multicast Networking and IGMP

sACN traffic is sent to a range of multicast addresses reserved for this purpose: 239.255.0.0/16. The specific address for a given DMX universe is calculated by adding the universe number to the base address.

  • Universe 1: 239.255.0.1
  • Universe 2: 239.255.0.2
  • …and so on.

A node “subscribes” to a multicast group using the IGMP (Internet Group Management Protocol). Our ESP32’s network stack handles the low-level IGMP messages for us; we simply need to use the correct socket options in our code to tell it which multicast group we want to join. sACN communication occurs on UDP port 5568.

3. Network Byte Order (Big-Endian)

This is arguably the most critical difference for a programmer. Network protocols, by convention, use network byte order, which is big-endian. This means for a multi-byte number (like a 16-bit universe or a 32-bit vector), the most significant byte comes first.

The ESP32, like most modern processors, is little-endian (least significant byte first).

Analogy: Think of writing the number 258. In little-endian (what the ESP32 uses internally), it might be stored in memory as [0x02, 0x01]. In big-endian (network order), it’s stored as [0x01, 0x02].

This means when we receive an sACN packet, we cannot simply cast it to a struct and read the values. We must use conversion functions like ntohs() (network to host short) and ntohl() (network to host long) to swap the byte order correctly.

4. The sACN Packet Structure

An sACN packet is composed of three distinct layers, all contained within a single UDP datagram.

Practical Examples

This example will create a single-universe sACN node that joins a multicast group and drives a DMX fixture.

Prerequisites

  1. Hardware: Identical to the Art-Net chapter (ESP32, MAX485, XLR connector, DMX fixture).
  2. Software:
    • VS Code with ESP-IDF v5.x.
    • Art-Net controller software that also supports sACN, like QLC+.

Step 1: Hardware & DMX Driver

Assemble the same hardware as in Chapter 214. Create the same dmx_driver.c and dmx_driver.h files in your project. The DMX output stage is identical.

Step 2: Project Setup

  1. Create a new ESP-IDF project.
  2. Use idf.py menuconfig to set your Wi-Fi SSID and Password.
  3. Enable IGMP in the kernel: menuconfig -> Component config -> LWIP -> Enable IGMP.

Step 3: Writing the Main Application Logic

flowchart TD
    subgraph "Initialization"
        A[Start] --> B{"Setup Wi-Fi & <br> DMX Driver"};
    end
    
    subgraph "sACN Task"
        B --> C{"Create UDP Socket"};
        C --> D{"Calculate Multicast IP<br>for Universe"};
        D --> E{"Join Multicast Group<br>(setsockopt)"};
        E --> F{"Bind Socket to<br>Port 5568"};
        F --> G{Listen for UDP Packet};
        G --> H{Packet Received?};
        H -- No --> G;
        H -- Yes --> I{"Validate Packet"};
    end
    
    subgraph "Packet Validation"
        I --> J{"Vector == 4?<br>(Data Packet)"};
        J -- No --> G;
        J -- Yes --> K{"Universe == Our Universe?<br>(ntohs)"};
        K -- No --> G;
        K -- Yes --> L{"DMP Vector == 0x02?<br>(DMX Data)"};
        L -- No --> G;
        L -- Yes --> M{"START Code == 0x00?"};
        M -- No --> G;
    end

    subgraph "Output"
        M -- Yes --> N["Extract DMX Data & Length<br>(ntohs)"];
        N --> O["Send Data to<br>DMX Bus"];
        O --> G;
    end
    
    classDef start-end fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
    classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef io fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B;
    classDef init fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;

    class A,O,N init;
    class B,C,D,E,F,I,M process;
    class G,H,J,K,L decision;
    class N,O io;

The main application will look similar to our Art-Net node, but the socket setup and packet parsing logic are critically different.

C
/* main/sacn_node_main.c */
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "lwip/err.h"
#include "lwip/sockets.h"
#include "lwip/sys.h"
#include <lwip/netdb.h>

#include "dmx_driver.h" // Our DMX driver from previous chapter

#define SACN_PORT 5568
#define SACN_UNIVERSE 1 // Listen for Universe 1 (standard is 1-based)
#define SACN_MULTICAST_IP_PREFIX "239.255.0."

static const char *TAG = "SACN_NODE";

// sACN Packet Structures
#pragma pack(push, 1)
typedef struct {
    // Root Layer
    uint16_t preamble_size;
    uint16_t postamble_size;
    uint8_t acn_packet_id[12];
    // Framing Layer
    uint16_t flags_and_length;
    uint32_t vector;
    uint8_t source_name[64];
    uint8_t priority;
    uint16_t reserved;
    uint8_t sequence_number;
    uint8_t options;
    uint16_t universe;
    // DMP Layer
    uint16_t dmp_flags_and_length;
    uint8_t dmp_vector;
    uint8_t address_and_data_type;
    uint16_t first_property_address;
    uint16_t address_increment;
    uint16_t property_value_count;
    uint8_t start_code;
    uint8_t dmx_data[DMX_CHANNEL_COUNT];
} sacn_packet_t;
#pragma pack(pop)

// Forward declare wifi_init_sta from previous chapter
void wifi_init_sta(void); 

static void sacn_receive_task(void *pvParameters) {
    char rx_buffer[1500];
    char multicast_ip_str[20];
    struct sockaddr_in dest_addr;

    // --- 1. Set up Multicast Address ---
    sprintf(multicast_ip_str, "%s%d", SACN_MULTICAST_IP_PREFIX, SACN_UNIVERSE);
    
    struct sockaddr_in *dest_addr_ip4 = (struct sockaddr_in *)&dest_addr;
    dest_addr_ip4->sin_addr.s_addr = htonl(INADDR_ANY);
    dest_addr_ip4->sin_family = AF_INET;
    dest_addr_ip4->sin_port = htons(SACN_PORT);
    int ip_protocol = IPPROTO_IP;

    int sock = socket(AF_INET, SOCK_DGRAM, ip_protocol);
    if (sock < 0) {
        ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
        vTaskDelete(NULL);
        return;
    }

    // --- 2. Join the Multicast Group ---
    struct ip_mreq imreq;
    imreq.imr_multiaddr.s_addr = inet_addr(multicast_ip_str);
    imreq.imr_interface.s_addr = htonl(INADDR_ANY);
    if (setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &imreq, sizeof(imreq)) < 0) {
        ESP_LOGE(TAG, "Failed to join multicast group");
        close(sock);
        vTaskDelete(NULL);
        return;
    }
    ESP_LOGI(TAG, "Joined multicast group %s", multicast_ip_str);

    int err = bind(sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
    if (err < 0) {
        ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
        close(sock);
        vTaskDelete(NULL);
        return;
    }
    ESP_LOGI(TAG, "Socket bound on port %d", SACN_PORT);

    while (1) {
        struct sockaddr_storage source_addr;
        socklen_t socklen = sizeof(source_addr);
        int len = recvfrom(sock, rx_buffer, sizeof(rx_buffer) - 1, 0, (struct sockaddr *)&source_addr, &socklen);

        if (len < 0) {
            ESP_LOGE(TAG, "recvfrom failed: errno %d", errno);
            break;
        } else if (len > 0) {
            sacn_packet_t *packet = (sacn_packet_t *)rx_buffer;

            // --- 3. Validate Packet and Convert from Network Byte Order ---
            uint16_t universe = ntohs(packet->universe);
            uint32_t root_vector = ntohl(packet->vector);

            // Check if it's a data packet for our universe
            if (root_vector == 4 && universe == SACN_UNIVERSE) {
                // Check DMP vector to ensure it's DMX data
                if (packet->dmp_vector == 0x02) {
                    uint16_t data_len = ntohs(packet->property_value_count) - 1;
                    if (data_len > 0 && packet->start_code == 0x00) {
                        ESP_LOGD(TAG, "sACN for universe %d, seq %d, len %d", 
                                 universe, packet->sequence_number, data_len);
                        
                        // --- 4. Send to DMX bus ---
                        dmx_send_data(packet->dmx_data, data_len);
                    }
                }
            }
        }
    }

    close(sock);
    vTaskDelete(NULL);
}


void app_main(void) {
    // Initialize NVS
    ESP_ERROR_CHECK(nvs_flash_init());
    
    // Initialize networking
    wifi_init_sta(); // This function should be copied from the previous chapter

    // Initialize DMX driver
    dmx_driver_init();
    
    // Start the UDP receiver task
    xTaskCreate(sacn_receive_task, "sacn_task", 4096, NULL, 5, NULL);
}

// NOTE: You must also copy the wifi_init_sta function and the dmx_driver files
// from the previous chapter (214) into your project for this to compile.

Step 4: Build, Flash, and Test

  1. Copy the wifi_init_stadmx_driver.c, and dmx_driver.h files from the Art-Net project.
  2. Build the project (idf.py build).
  3. Flash the firmware (idf.py -p [YOUR_PORT] flash).
  4. Monitor the output. You should see the ESP32 connect to Wi-Fi and log that it has joined the multicast group 239.255.0.1.
  5. Configure QLC+:
    • Go to the “Inputs/Outputs” tab.
    • Select “E1.31 (sACN)” and check the box under “Output” for your network adapter.
    • Go to the “Simple Desk” tab.
    • Ensure Universe 1 in QLC+ is selected.
    • Move the faders for your light fixture’s channels.
  6. Observe: Your DMX light should respond instantly, driven by sACN data.

Variant Notes

  • Network Performance: The conclusions from the Art-Net chapter hold true here. Any Wi-Fi enabled ESP32 can handle a single-universe sACN node with ease. For professional, multi-universe, or low-latency applications, a wired connection using the original ESP32’s Ethernet MAC or a SPI Ethernet module (like W5500) on any other variant is strongly recommended. Multicast performance is generally better than broadcast on a well-configured network, reducing the overall load on the ESP32’s Wi-Fi stack.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Troubleshooting / Solution
Forgetting Byte Order Conversion Packet validation logic like
if (packet->universe == 1) never evaluates to true. Serial monitor shows packets are received, but they are never processed.
Always convert from Network to Host byte order. Wrap multi-byte fields from the network packet in conversion functions before using them.

Incorrect:
if (packet->universe == SACN_UNIVERSE)

Correct:
if (ntohs(packet->universe) == SACN_UNIVERSE)

Use ntohs() for 16-bit values (short) and ntohl() for 32-bit values (long).
Multicast Not Enabled or Blocked The ESP32 logs that it successfully bound the socket, but no packets are ever received (recvfrom never returns). The controller software shows it is sending data. 1. Enable IGMP in ESP-IDF: Run idf.py menuconfig and go to:
Component config -> LWIP -> Enable IGMP.

2. Check Router Settings: Log into your Wi-Fi router’s admin page. Look for settings like “Multicast Filtering,” “IGMP Snooping,” or “IGMP Proxy.” Try enabling these features. Some consumer routers block multicast by default.
Incorrect Multicast IP Address Same as above: the node appears to work but receives no data. Verify the IP calculation. sACN Universe 1 is sent to multicast address 239.255.0.1. Ensure your code correctly generates this string. A common mistake is an off-by-one error, sending to …0.0 for Universe 1. Check the log output for “Joined multicast group…” to see the exact IP your node is listening on.
PC Firewall Blocking Controller The ESP32 node works perfectly (verified with another controller or a mobile app), but no data arrives when sending from your primary PC software (e.g., QLC+). Create a firewall exception. When you first run your sACN software, your OS (Windows/macOS) should prompt you to allow it network access. If you denied it or ignored the prompt, you must manually go to your firewall settings and create a rule to allow your controller application (e.g., qlcplus.exe) to send and receive UDP traffic, especially on private/local networks.
Incorrect #pragma pack usage DMX data is garbled or shifted. Some fields like sequence_number might seem correct, but channel values are wrong. The issue is often inconsistent. Ensure your struct definition is wrapped correctly with #pragma pack(push, 1) and #pragma pack(pop). This tells the C compiler to not add any padding bytes between struct members, ensuring the C struct layout exactly matches the network packet’s byte layout. Without it, the compiler might align members on 2- or 4-byte boundaries, misinterpreting the entire packet after the first unaligned member.

Exercises

  1. Implement Priority Handling: Modify the sacn_receive_task to keep track of the source_name and priority of the last received packet. If a new packet arrives from a different source with a lower priority, ignore it. If a packet arrives with a higher priority, switch to accepting data from that source.
  2. Data Timeout: Implement a timeout mechanism. If no sACN packets for your universe are received within 3 seconds, call dmx_send_data with a zeroed-out buffer to turn the lights off. This is a crucial safety feature. You can achieve this using a FreeRTOS timer or by tracking xTaskGetTickCount().
  3. Web-Based Configuration: Add a web server that allows the user to change the sACN universe the node listens to. Store this value in NVS and use it to calculate the multicast IP address on startup.
  4. Discovery with sACN E1.17 (Advanced): sACN has a companion discovery protocol, E1.17 (ACN Discovery or “Discovery LTP”). Research this protocol and implement a basic responder so your ESP32 node can be automatically discovered by professional lighting consoles.

Summary

  • sACN (E1.31) is an ANSI standard for DMX over IP, favored in professional environments.
  • It primarily uses IP multicast on UDP port 5568, which is more efficient than broadcast for large networks.
  • Multicast addresses are in the 239.255.0.0/16 range, with the last octet typically matching the universe number.
  • sACN packets use network byte order (big-endian), requiring conversion with ntohs()/ntohl() on little-endian processors like the ESP32.
  • The protocol features a layered packet structure and supports advanced features like source priority.

Further Reading

  • ANSI E1.31-2018 Standard Document: The official specification. You can find it on the ESTA (Entertainment Services and Technology Association) website.
  • ESP-IDF LwIP Socket Documentation: Specifically look for examples of setting socket options like IP_ADD_MEMBERSHIP. (https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/lwip.html)
  • Introduction to IP Multicast: Many great online tutorials explain the concepts of IGMP, multicast routing, and address ranges.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top