Instructions for building the Rekjndäl Glueless WiFi Art-Net Node.
Connect the DMX module to the Arduino Nano ESP32 as follows:
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();
}
}