Rekjndäl Glueless WiFi Art-Net Node

Instructions for building the Rekjndäl Glueless WiFi Art-Net Node.

Rekjndäl Glueless WiFi Art-Net Node

Parts

Tools

Wiring

Connect the DMX module to the Arduino Nano ESP32 as follows:

Code

Upload the following code to your Arduino Nano ESP32 using the Arduino IDE.


#include <Arduino.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include <string.h>

constexpr bool SERIAL_LOG_ENABLED = true;

const char WIFI_SSID[] = "your-wifi-ssid";
const char WIFI_PASSWORD[] = "your-wifi-password";

constexpr uint16_t ARTNET_PORT = 6454;
constexpr uint16_t ARTNET_UNIVERSE = 0;

constexpr uint8_t DMX_TX_PIN = A6;
constexpr uint16_t DMX_BREAK_US = 120;
constexpr uint16_t DMX_MAB_US = 12;
constexpr unsigned long DMX_RESEND_INTERVAL_MS = 25; // ~40 Hz, matching typical Art-Net nodes

WiFiUDP artnetUdp;

uint8_t dmxData[512] = {0};
uint16_t dmxLength = 512;
bool haveFrame = false;
unsigned long lastSendMs = 0;

constexpr size_t ARTNET_PACKET_MAX_BYTES = 530; // header (18) + up to 512 channels
uint8_t packetBuffer[ARTNET_PACKET_MAX_BYTES];

bool parseArtDmx(const uint8_t *packet, int length) {
    constexpr int HEADER_BYTES = 18;
    if (length < HEADER_BYTES) return false;
    if (memcmp(packet, "Art-Net", 7) != 0 || packet[7] != 0) return false;

    uint16_t opcode = packet[8] | (packet[9] << 8); // little-endian per spec
    if (opcode != 0x5000) return false; // only ArtDMX is relevant here

    uint8_t subUni = packet[14];
    uint8_t net = packet[15];
    uint16_t universe = static_cast(subUni) | (static_cast(net) << 8);
    if (universe != ARTNET_UNIVERSE) return false;

    uint16_t channelCount = (static_cast(packet[16]) << 8) | packet[17];
    if (channelCount == 0 || channelCount > 512) return false;
    if (length < HEADER_BYTES + channelCount) return false;

    memcpy(dmxData, packet + HEADER_BYTES, channelCount);
    if (channelCount < sizeof(dmxData)) {
        memset(dmxData + channelCount, 0, sizeof(dmxData) - channelCount);
    }
    dmxLength = channelCount;
    return true;
}

void sendDmxFrame() {
    Serial1.flush();
    Serial1.end();
    pinMode(DMX_TX_PIN, OUTPUT);
    digitalWrite(DMX_TX_PIN, LOW);
    delayMicroseconds(DMX_BREAK_US);
    digitalWrite(DMX_TX_PIN, HIGH);
    delayMicroseconds(DMX_MAB_US);
    
    Serial1.begin(250000, SERIAL_8N2, -1, DMX_TX_PIN);

    Serial1.write(static_cast(0)); // start code
    Serial1.write(dmxData, dmxLength);

    lastSendMs = millis();
}

void connectToWiFi() {
    WiFi.mode(WIFI_STA);
    WiFi.setSleep(false); // modem sleep adds tens-to-hundreds of ms of latency
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    if (SERIAL_LOG_ENABLED) {
        Serial.printf("Connecting to \"%s\"", WIFI_SSID);
    }
    while (WiFi.status() != WL_CONNECTED) {
        delay(250);
        if (SERIAL_LOG_ENABLED) Serial.print('.');
    }
    if (SERIAL_LOG_ENABLED) {
        Serial.println();
        Serial.printf("Connected, IP %s\n", WiFi.localIP().toString().c_str());
    }
}

void setup() {
    Serial.begin(115200);
    unsigned long serialWaitStartMs = millis();
    while (!Serial && (millis() - serialWaitStartMs) < 3000) {
        delay(10);
    }

    connectToWiFi();
    artnetUdp.begin(ARTNET_PORT);
    if (SERIAL_LOG_ENABLED) {
        Serial.printf("Listening for Art-Net universe %u on UDP %u\n", ARTNET_UNIVERSE, ARTNET_PORT);
    }
}

void loop() {
    if (WiFi.status() != WL_CONNECTED) {
        connectToWiFi();
    }

    int packetSize = artnetUdp.parsePacket();
    if (packetSize > 0 && packetSize <= static_cast(sizeof(packetBuffer))) {
        int readBytes = artnetUdp.read(packetBuffer, sizeof(packetBuffer));
        if (parseArtDmx(packetBuffer, readBytes)) {
        haveFrame = true;
        sendDmxFrame();
        }
    }

    if (haveFrame && (millis() - lastSendMs) >= DMX_RESEND_INTERVAL_MS) {
        sendDmxFrame();
    }
}