├── _config.yml ├── images ├── Config.png ├── Relay.jpg ├── Status.png ├── Test.png ├── Wemos.jpg └── GPIORelay-Wiring.jpg ├── ESP-131GPIO ├── data │ ├── favicon.png │ └── index.html └── ESP-131GPIO.ino ├── .gitignore ├── .vscode └── extensions.json ├── platformio.ini └── README.md /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /images/Config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garymueller/ESP-131GPIO/HEAD/images/Config.png -------------------------------------------------------------------------------- /images/Relay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garymueller/ESP-131GPIO/HEAD/images/Relay.jpg -------------------------------------------------------------------------------- /images/Status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garymueller/ESP-131GPIO/HEAD/images/Status.png -------------------------------------------------------------------------------- /images/Test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garymueller/ESP-131GPIO/HEAD/images/Test.png -------------------------------------------------------------------------------- /images/Wemos.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garymueller/ESP-131GPIO/HEAD/images/Wemos.jpg -------------------------------------------------------------------------------- /images/GPIORelay-Wiring.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garymueller/ESP-131GPIO/HEAD/images/GPIORelay-Wiring.jpg -------------------------------------------------------------------------------- /ESP-131GPIO/data/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garymueller/ESP-131GPIO/HEAD/ESP-131GPIO/data/favicon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "platformio.platformio-ide" 6 | ], 7 | "unwantedRecommendations": [ 8 | "ms-vscode.cpptools-extension-pack" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | [platformio] 2 | src_dir = ./ESP-131GPIO 3 | data_dir = ./ESP-131GPIO/data 4 | 5 | [env] 6 | upload_speed = 921600 7 | monitor_speed = 115200 8 | 9 | [esp8266] 10 | framework = arduino 11 | platform = espressif8266 12 | board_build.filesystem = littlefs 13 | lib_deps = 14 | forkineye/ESPAsyncE131 @ ^1.0.4 15 | ESP Async WebServer @ ^1.2.3 16 | devyte/ESPAsyncDNSServer @ ^1.0.0 17 | bblanchon/ArduinoJson @ ^6.19.4 18 | 19 | [env:d1_mini] 20 | extends = esp8266 21 | board = d1_mini 22 | build_flags = 23 | -D BOARD_NAME='"d1_mini"' 24 | 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP-131GPIO 2 | 3 | This is arduino firmware that will listen to a E1.31 (sACN) data soure and will drive a boards GPIO pins. The firmware supports ESP8266 and is currently configured to support the WEMOS mini. This was designed to drive relays for static christmas display lights being driven by Vixen, XLights or other application that output a E1.31 data stream. The board supports the following ... 4 | 5 | * Web Interface 6 | * Status page 7 | * Configuration Page 8 | * Network 9 | * E131 10 | * GPIO 11 | * Test Page 12 | 13 | * REST API - to set relay states through an external interface 14 | * /SetRelay?relay=[0-#]&checked=[true|false] 15 | * Captive Portal (Optional) - Automatically start the arduino in access point mode if the WiFi settings aren't able to connect. 16 | * Digital/PWM support 17 | 18 | # Hardware 19 | 20 | [WeMos D1 Mini](https://www.amazon.com/IZOKEE-NodeMcu-Internet-Development-Compatible/dp/B076F52NQD) 21 | 22 | 23 | 24 | [8-Channel 5V Solid State Relay](https://www.amazon.com/gp/product/B006J4G45G) 25 | 26 | 27 | 28 | # Wiring 29 | 30 | 31 | 32 | # Build Environments 33 | 34 | There are 2 ways to build the software. The first is to use the arduino ide and the second is to use platformio. The required libraries for the arduino ide environment are listed below and must be installed in order to compile. The Platformio environment is easier to use as the libraries should automatically be downloaded and made available to the environment for you. 35 | 36 | [Arduino IDE](https://www.arduino.cc/en/main/software) 37 | 38 | [Arduino 8266](https://github.com/esp8266/Arduino) 39 | 40 | [arduino-esp8266littlefs-plugin](https://github.com/earlephilhower/arduino-esp8266littlefs-plugin/releases) 41 | 42 | or 43 | 44 | [Platformio](https://platformio.org/install) 45 | 46 | # Arduino IDE Libraries 47 | [Arduino WiFi](https://www.arduino.cc/en/Reference/WiFi) 48 | 49 | [Arduino JSON](https://arduinojson.org/) 50 | 51 | [ESPAsyncE131](https://github.com/forkineye/ESPAsyncE131) 52 | 53 | [ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer) 54 | 55 | [ESPAsyncDNSServer](https://github.com/devyte/ESPAsyncDNSServer) 56 | 57 | [ESPAsyncTCP](https://github.com/me-no-dev/ESPAsyncTCP) 58 | 59 | [ESPAsyncUDP](https://github.com/me-no-dev/ESPAsyncUDP) 60 | 61 | # Software Installation 62 | 63 | Download and install the above build environments and libraries. The build environments can be installed by downloading and installing the appropriate software. 64 | * **Arduino IDE** - The libraries can be installed through the ide with manage libraries. 65 | 66 | # Firmware Installion 67 | 1. The Wemos requires you to jump pins D3 and GND to start the board into programming mode. 68 | 2. Plug the arduino into your computers USB. 69 | 3. Click the upload button to compile and transfer the firmware to the arduino. 70 | 4. Upload data files 71 | * **Arduino IDE** 72 | - select the "ESP8266 LittleFS data upload" found under the tools menu. This will upload the data directory (html) to the arduino. 73 | * **PlaformIO** 74 | - Click the PlatformIO icon in the sidebar 75 | - Expand the Platform menu. 76 | - Select Build Filesystem Image. 77 | - click Upload Filesystem Image. 78 | 79 | # Web Interface 80 | 81 | 82 | 83 | 84 | 85 | # Acknowledgements 86 | I wish to thank the following individuals / groups were instrumental into helping make this project. 87 | * [Shelby Merrick](https://github.com/forkineye) 88 | * [ESPixelStick](https://github.com/forkineye/ESPixelStick) 89 | * [E131](https://github.com/forkineye/E131) 90 | * [doityourselfchistmas](http://doityourselfchristmas.com/forums/archive/index.php/t-51293.html) 91 | * many others with various examples, tutorials, etc. 92 | -------------------------------------------------------------------------------- /ESP-131GPIO/data/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | %HOSTNAME% 4 | 5 | 6 | 106 | 107 | 108 | 109 |

%HOSTNAME%

110 | 111 |
112 | 113 | 114 | 115 |
116 | 117 |
118 |

Network Status

119 |
120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 |
SSID: %SSID%
Hostname: %HOSTNAME%
IP: %IP%
MAC: %MAC%
RSSI: %RSSI%
Free Heap: %HEAP%
Up Time:
130 | 131 |

E1.31 Status

132 |
133 | 134 | 135 | 136 | 137 | 138 | 139 |
Universe: %UNIVERSE%
Total Packets: %PACKETS%
Paket Errors: %PACKET_ERR%
Last IP: %LAST_IP%
140 | 141 |
142 | 143 |
144 |
145 | 146 |

Network

147 | 148 | 149 | 150 |

151 | 152 | 153 | 154 |

155 | 156 | 157 | 158 |

159 | 160 | 161 | 162 |

163 | 164 | 165 | 166 |

167 | 168 |
169 | 170 | 171 | 172 |

173 | 174 | 175 | 176 |

177 | 178 | 179 | 180 |

181 | 182 |
183 | 184 | 185 |

E1.31

186 |
187 | 188 | 189 | 190 |

191 | 192 | 193 | 194 |

195 | 196 |
197 | 198 | 199 |

200 |
201 | 202 |

GPIO

203 |
204 | 205 | 206 | 207 |

208 | 209 |
210 | 211 | 212 | 213 |

214 | 215 | 216 | 217 |

218 |
219 | 220 | 221 |
222 |
223 | 224 |
225 | 226 | %RELAYS% 227 | 228 |
229 | 230 | 276 | 277 | 278 | 279 | -------------------------------------------------------------------------------- /ESP-131GPIO/ESP-131GPIO.ino: -------------------------------------------------------------------------------- 1 | //https://github.com/garymueller/ESP-131GPIO 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | using namespace std; 14 | 15 | // ***** USER SETUP STUFF ***** 16 | String ssid = "SSID"; // replace with your SSID. 17 | String password = "Password"; // replace with your password. 18 | 19 | String CONFIG_FILE = "Config.json"; 20 | DynamicJsonDocument Config(1024); 21 | AsyncWebServer server(80); 22 | AsyncDNSServer dnsServer; 23 | ESPAsyncE131 e131; 24 | 25 | //for Wemos D1 R1 pins are 16,5,4,14,12,13,0,2 26 | //vector GpioVector{16,5,4,14,12,13,0,2}; 27 | //for Wemos D1 R2 pins are 16,5,4,0,2,14,12,13 28 | vector GpioVector{16,5,4,0,2,14,12,13}; 29 | int DigitalOn = HIGH; 30 | int DigitalOff = LOW; 31 | 32 | //Forward Declarations 33 | String WebReplace(const String& var); 34 | 35 | bool LoadConfig(); 36 | void SaveConfig(AsyncWebServerRequest* request); 37 | 38 | void InitGpio(); 39 | void InitWifi(); 40 | void Init131(); 41 | void InitWeb(); 42 | 43 | /* 44 | * Sets up the initial state and starts sockets 45 | */ 46 | void setup() { 47 | Serial.begin(115200); 48 | delay(500); 49 | Serial.println("=============================="); 50 | Serial.println("Start Setup"); 51 | 52 | LoadConfig(); 53 | 54 | InitGpio(); 55 | 56 | InitWifi(); 57 | 58 | Init131(); 59 | 60 | InitWeb(); 61 | 62 | Serial.println("Completed setup. Starting loop"); 63 | Serial.println("=============================="); 64 | } 65 | 66 | /* 67 | * Main Event Loop 68 | */ 69 | void loop() { 70 | e131_packet_t packet; 71 | bool Digital = Config["GPIO"]["digital"].as(); 72 | int DigitalThreshold = Config["GPIO"]["digital_threshold"].as(); 73 | int ChannelOffset = Config["E131"]["channel_offset"].as(); 74 | 75 | while(!e131.isEmpty()) { 76 | e131.pull(&packet); // Pull packet from ring buffer 77 | uint16_t num_channels = htons(packet.property_value_count) - 1; 78 | 79 | for(int GpioIndex = 0, ChannelIndex = ChannelOffset; 80 | GpioIndex < GpioVector.size() && ChannelIndex < num_channels; 81 | ++GpioIndex, ++ChannelIndex) { 82 | 83 | uint16_t data = packet.property_values[ChannelIndex+1]; 84 | if(Digital) { 85 | digitalWrite(GpioVector[GpioIndex], (data >= DigitalThreshold) ? DigitalOn : DigitalOff); 86 | } else { 87 | analogWrite(GpioVector[GpioIndex], data); 88 | } 89 | //Serial.printf("%d:%d:%d; ",GpioIndex,data,(data > DigitalThreshold) ? DigitalOn : DigitalOff); 90 | }//for loop 91 | //Serial.println(""); 92 | }//! empty 93 | }// end void loop 94 | 95 | 96 | /* 97 | * InitGpio 98 | */ 99 | void InitGpio() 100 | { 101 | //initialize GPIO pins 102 | for(int i = 0; i < GpioVector.size(); ++i) { 103 | pinMode(GpioVector[i], OUTPUT); 104 | digitalWrite(GpioVector[i], DigitalOff); 105 | } 106 | } 107 | 108 | /* 109 | * Loads the config from SPPS into the Config variable 110 | */ 111 | bool LoadConfig() 112 | { 113 | // Initialize LittleFS 114 | if(!LittleFS.begin()){ 115 | Serial.println("An Error has occurred while mounting LittleFS"); 116 | return false; 117 | } 118 | 119 | File file = LittleFS.open(CONFIG_FILE, "r"); 120 | if (!file) { 121 | //First Run, setup defaults 122 | Config["network"]["hostname"] = "esps-" + String(ESP.getChipId(), HEX); 123 | Config["network"]["ssid"] = ssid; 124 | Config["network"]["password"] = password; 125 | Config["network"]["static"] = false; 126 | Config["network"]["static_ip"] = "192.168.1.100"; 127 | Config["network"]["static_netmask"] = "255.255.255.0"; 128 | Config["network"]["static_gateway"] = "192.168.1.1"; 129 | Config["network"]["access_point"] = true; 130 | 131 | Config["E131"]["multicast"] = "true"; 132 | Config["E131"]["universe"] = 1; 133 | Config["E131"]["channel_offset"] = 0; 134 | 135 | Config["GPIO"]["digital"] = "true"; 136 | Config["GPIO"]["digital_threshold"] = 127; 137 | Config["GPIO"]["digital_lowlevel"] = false; 138 | 139 | } else { 140 | Serial.println("Loading Configuration File"); 141 | deserializeJson(Config, file); 142 | } 143 | 144 | if(Config["GPIO"]["digital_lowlevel"].as()) { 145 | DigitalOn = LOW; 146 | DigitalOff = HIGH; 147 | } else { 148 | DigitalOn = HIGH; 149 | DigitalOff = LOW; 150 | } 151 | 152 | serializeJson(Config, Serial);Serial.println(); 153 | return true; 154 | } 155 | 156 | void SaveConfig(AsyncWebServerRequest* request) 157 | { 158 | Serial.println("Saving Configuration File"); 159 | 160 | //debug 161 | int params = request->params(); 162 | for(int i=0;igetParam(i); 164 | Serial.printf("Param: %s, %s\n", p->name().c_str(), p->value().c_str()); 165 | } 166 | 167 | //Validate Config 168 | 169 | Config["network"]["hostname"] = request->getParam("hostname",true)->value(); 170 | Config["network"]["ssid"] = request->getParam("ssid",true)->value(); 171 | Config["network"]["password"] = request->getParam("password",true)->value(); 172 | //checkbox status isnt always included if toggled off 173 | Config["network"]["static"] = 174 | (request->hasParam("static",true) && (request->getParam("static",true)->value() == "on")); 175 | Config["network"]["static_ip"] = request->getParam("static_ip",true)->value(); 176 | Config["network"]["static_netmask"] = request->getParam("static_netmask",true)->value(); 177 | Config["network"]["static_gateway"] = request->getParam("static_gateway",true)->value(); 178 | Config["network"]["access_point"] = 179 | (request->hasParam("access_point",true) && (request->getParam("access_point",true)->value() == "on")); 180 | 181 | //checkbox status isnt always included if toggled off 182 | Config["E131"]["multicast"] = 183 | (request->hasParam("multicast",true) && (request->getParam("multicast",true)->value() == "on")); 184 | Config["E131"]["universe"] = request->getParam("universe",true)->value(); 185 | Config["E131"]["channel_offset"] = request->getParam("channel_offset",true)->value(); 186 | 187 | //checkbox status isnt always included if toggled off 188 | Config["GPIO"]["digital"] = 189 | (request->hasParam("digital",true) && (request->getParam("digital",true)->value() == "on")); 190 | Config["GPIO"]["digital_threshold"] = request->getParam("digital_threshold",true)->value(); 191 | Config["GPIO"]["digital_lowlevel"] = 192 | (request->hasParam("digital_lowlevel",true) && (request->getParam("digital_lowlevel",true)->value() == "on")); 193 | 194 | File file = LittleFS.open(CONFIG_FILE, "w"); 195 | if(file) { 196 | serializeJson(Config, file); 197 | serializeJson(Config, Serial); 198 | } 199 | 200 | request->send(200); 201 | ESP.restart(); 202 | } 203 | 204 | /* 205 | * Initialiaze Wifi (DHCP/STATIC and Access Point) 206 | */ 207 | void InitWifi() 208 | { 209 | // Switch to station mode and disconnect just in case 210 | WiFi.mode(WIFI_STA); 211 | WiFi.disconnect(); 212 | 213 | WiFi.hostname(Config["network"]["hostname"].as()); 214 | 215 | //configure Static/DHCP 216 | if(Config["network"]["static"].as()) { 217 | IPAddress IP; IP.fromString(Config["network"]["static_ip"].as()); 218 | IPAddress Netmask; Netmask.fromString(Config["network"]["static_netmask"].as()); 219 | IPAddress Gateway; Gateway.fromString(Config["network"]["static_gateway"].as()); 220 | 221 | if(WiFi.config(IP, Netmask, Gateway)) { 222 | Serial.println("Successfully configured static IP"); 223 | } else { 224 | Serial.println("Failed to configure static IP"); 225 | } 226 | } else { 227 | Serial.println("Connecting with DHCP"); 228 | } 229 | 230 | //Connect 231 | int Timeout = 15000; 232 | WiFi.begin(Config["network"]["ssid"].as(), Config["network"]["password"].as()); 233 | if(WiFi.waitForConnectResult(Timeout) != WL_CONNECTED) { 234 | if(Config["network"]["access_point"].as()) { 235 | Serial.println("*** FAILED TO ASSOCIATE WITH AP, GOING SOFTAP ***"); 236 | WiFi.mode(WIFI_AP); 237 | WiFi.softAP(Config["network"]["hostname"].as()); 238 | dnsServer.start(53, "*", WiFi.softAPIP()); 239 | } else { 240 | Serial.println(F("*** FAILED TO ASSOCIATE WITH AP, REBOOTING ***")); 241 | ESP.restart(); 242 | } 243 | } else { 244 | Serial.printf("Connected as %s\n",WiFi.localIP().toString().c_str()); 245 | } 246 | 247 | WiFi.printDiag(Serial); 248 | } 249 | 250 | void Init131() 251 | { 252 | if(Config["E131"]["multicast"].as()) { 253 | Serial.printf("Initializing Multicast with universe <%d>\n",Config["E131"]["universe"].as()); 254 | e131.begin(E131_MULTICAST, Config["E131"]["universe"]); 255 | } else { 256 | Serial.println("Initializing Unicast"); 257 | e131.begin(E131_UNICAST); 258 | } 259 | } 260 | 261 | 262 | /* 263 | * Initializes the webserver 264 | */ 265 | void InitWeb() 266 | { 267 | //enables redirect to /index.html on AP connection 268 | server.onNotFound([](AsyncWebServerRequest *request){ 269 | request->send(LittleFS, "/index.html", String(), false, WebReplace); 270 | }); 271 | 272 | server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ 273 | request->send(LittleFS, "/index.html", String(), false, WebReplace); 274 | }); 275 | server.on("/favicon.ico", HTTP_GET, [](AsyncWebServerRequest *request){ 276 | request->send(LittleFS, "/favicon.png", "image/png"); 277 | }); 278 | server.on("/SaveConfig", HTTP_POST, [](AsyncWebServerRequest *request){ 279 | SaveConfig(request); 280 | }); 281 | server.on("/SetRelay", HTTP_GET, [](AsyncWebServerRequest *request){ 282 | int relay = request->getParam("relay")->value().toInt(); 283 | if(relay<0 || relay >= GpioVector.size()) { 284 | Serial.println("SetRelay - Index out of range"); 285 | return; 286 | } 287 | digitalWrite(GpioVector[relay], (request->getParam("checked")->value() == "true") ? DigitalOn : DigitalOff); 288 | request->send(200); 289 | }); 290 | 291 | server.begin(); 292 | } 293 | 294 | /* 295 | * WebReplace: 296 | * Substitutes variables inside of the html 297 | */ 298 | String WebReplace(const String& var) 299 | { 300 | //Status Page 301 | if (var == "SSID") { 302 | return (String)WiFi.SSID(); 303 | } else if (var == "HOSTNAME") { 304 | return (String)WiFi.hostname(); 305 | } else if (var == "IP") { 306 | if(WiFi.getMode() == WIFI_AP) { 307 | return WiFi.softAPIP().toString(); 308 | } else { 309 | return WiFi.localIP().toString(); 310 | } 311 | } else if (var == "MAC") { 312 | return (String)WiFi.macAddress(); 313 | } else if (var == "RSSI") { 314 | return (String)WiFi.RSSI(); 315 | } else if (var == "HEAP") { 316 | return (String)ESP.getFreeHeap(); 317 | } else if (var == "UPTIME") { 318 | return String(millis()); 319 | } else if (var == "UNIVERSE") { 320 | return Config["E131"]["universe"]; 321 | } else if (var == "PACKETS") { 322 | return (String)e131.stats.num_packets; 323 | } else if (var == "PACKET_ERR") { 324 | return (String)e131.stats.packet_errors; 325 | } else if (var == "LAST_IP") { 326 | return e131.stats.last_clientIP.toString(); 327 | 328 | //Configuration Page 329 | } else if (var == "CONFIG_HOSTNAME") { 330 | return Config["network"]["hostname"]; 331 | } else if (var == "CONFIG_SSID") { 332 | return Config["network"]["ssid"]; 333 | } else if (var == "CONFIG_PASSWORD") { 334 | return Config["network"]["password"]; 335 | } else if (var == "CONFIG_AP") { 336 | if(Config["network"]["access_point"].as()) 337 | return "checked"; 338 | else 339 | return ""; 340 | } else if (var == "CONFIG_STATIC") { 341 | if(Config["network"]["static"].as()) 342 | return "checked"; 343 | else 344 | return ""; 345 | } else if (var == "CONFIG_STATIC_IP") { 346 | return Config["network"]["static_ip"]; 347 | } else if (var == "CONFIG_STATIC_NETMASK") { 348 | return Config["network"]["static_netmask"]; 349 | } else if (var == "CONFIG_STATIC_GATEWAY") { 350 | return Config["network"]["static_gateway"]; 351 | } else if (var == "CONFIG_MULTICAST") { 352 | if(Config["E131"]["multicast"].as()) 353 | return "checked"; 354 | else 355 | return ""; 356 | } else if (var == "CONFIG_UNIVERSE") { 357 | return Config["E131"]["universe"]; 358 | } else if (var == "CONFIG_CHANNEL_OFFSET") { 359 | return Config["E131"]["channel_offset"]; 360 | } else if (var == "CONFIG_DIGITAL") { 361 | if(Config["GPIO"]["digital"].as()) 362 | return "checked"; 363 | else 364 | return ""; 365 | } else if (var == "CONFIG_THRESHOLD") { 366 | return Config["GPIO"]["digital_threshold"]; 367 | } else if (var == "CONFIG_LOWLEVEL") { 368 | if(Config["GPIO"]["digital_lowlevel"].as()) 369 | return "checked"; 370 | else 371 | return ""; 372 | 373 | //Relay Page 374 | } else if (var == "RELAYS") { 375 | String Relays = ""; 376 | for(int i = 0; i < GpioVector.size(); ++i) { 377 | Relays += ""; 378 | Relays += "