From ESP8266_ArtNetNode_v2, 10 Months ago, written in HTML5.
Embed
  1. /*
  2. ESP8266_ArtNetNode v2.0.0
  3. Copyright (c) 2016, Matthew Tong
  4. https://github.com/mtongnz/ESP8266_ArtNetNode_v2
  5.  
  6. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
  7. License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
  8. later version.
  9.  
  10. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
  11. warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License along with this program.
  13. If not, see http://www.gnu.org/licenses/
  14.  
  15. Note:
  16. This is a pre-release version of this software.  It is not yet ready for prime time and contains bugs (known and unknown).
  17. Please submit any bugs or code changes so they can be included into the next release.
  18.  
  19. Prizes up for grabs:
  20. I am giving away a few of my first batch of prototype PCBs.  They will be fully populated - valued at over $30 just for parts.
  21. In order to recieve one, please complete one of the following tasks.  You can "win" multiple boards.
  22. 1 - Fix the WDT reset issue (https://github.com/mtongnz/ESP8266_ArtNetNode_v2/issues/41)
  23. 2 - Implement stored scenes function.  I want it to allow for static scenes or for chases to run.
  24. 3 - Most bug fixes, code improvements, feature additions & helpful submissions.
  25.    eg. Fixing the flickering WS2812 (https://github.com/mtongnz/ESP8266_ArtNetNode_v2/issues/36)
  26.        Adding other pixel strips (https://github.com/mtongnz/ESP8266_ArtNetNode_v2/issues/42)
  27.        Creating new web UI theme (https://github.com/mtongnz/ESP8266_ArtNetNode_v2/issues/22)
  28.        
  29. These prizes will be based on the first person to submit a solution that I judge to be adequate.  My decision is final.
  30. This competition will open to the general public a couple of weeks after the private code release to supporters.
  31. */
  32.  
  33. #include <ESP8266WiFi.h>
  34. #include <WiFiClient.h>
  35. #include <ESP8266WebServer.h>
  36. #include <ArduinoJson.h>
  37. #include <EEPROM.h>
  38. #include <FS.h>
  39. #include "store.h"
  40. #include "espDMX_RDM.h"
  41. #include "espArtNetRDM.h"
  42. #include "ws2812Driver.h"
  43. #include "wsFX.h"
  44.  
  45. extern "C" {
  46.  #include "user_interface.h"
  47.  extern struct rst_info resetInfo;
  48. }
  49.  
  50. #define FIRMWARE_VERSION "v2.0.0 (beta 5g)"
  51. #define ART_FIRM_VERSION 0x0200   // Firmware given over Artnet (2 bytes)
  52.  
  53.  
  54. //#define ESP_01              // Un comment for ESP_01 board settings
  55. //#define NO_RESET            // Un comment to disable the reset button
  56.  
  57. #define Wemos boards use 4M (3M SPIFFS) compiler option
  58.  
  59.  
  60. #define ARTNET_OEM 0x0123    // Artnet OEM Code
  61. #define ESTA_MAN 0x08DD      // ESTA Manufacturer Code
  62. #define ESTA_DEV 0xEE000000  // RDM Device ID (used with Man Code to make 48bit UID)
  63.  
  64.  
  65.  
  66.  
  67.  
  68.  
  69. #ifdef ESP_01
  70.   #define DMX_DIR_A 2   // Same pin as TX1
  71.   #define DMX_TX_A 1
  72.   #define ONE_PORT
  73.   #define NO_RESET
  74.  
  75.   #define WS2812_ALLOW_INT_SINGLE false
  76.   #define WS2812_ALLOW_INT_DOUBLE false
  77.  
  78. #else
  79.   #define DMX_DIR_A 5   // D1
  80.   #define DMX_DIR_B 16  // D0
  81.   #define DMX_TX_A 1
  82.   #define DMX_TX_B 2
  83.  
  84.   #define STATUS_LED_PIN 12
  85.   #define STATUS_LED_MODE_WS2812
  86. //   #define STATUS_LED_MODE_APA106
  87.   #define STATUS_LED_A 0  // Physical wiring order for status LEDs
  88.   #define STATUS_LED_B 1
  89.   #define STATUS_LED_S 2
  90.  
  91.   #define WS2812_ALLOW_INT_SINGLE false
  92.   #define WS2812_ALLOW_INT_DOUBLE false
  93. #endif
  94.  
  95. #ifndef NO_RESET
  96.   #define SETTINGS_RESET 14
  97. #endif
  98.  
  99.  
  100. // Definitions for status leds  xxBBRRGG
  101. #define BLACK 0x00000000
  102. #define WHITE 0x00FFFFFF
  103. #define RED 0x0000FF00
  104. #define GREEN 0x000000FF
  105. #define BLUE 0x00FF0000
  106. #define CYAN 0x00FF00FF
  107. #define PINK 0x0066FF22
  108. #define MAGENTA 0x00FFFF00
  109. #define YELLOW 0x0000FFFF
  110. #define ORANGE 0x0000FF33
  111. #define STATUS_DIM 0x0F
  112.  
  113. uint8_t portA[5], portB[5];
  114. uint8_t MAC_array[6];
  115. uint8_t dmxInSeqID = 0;
  116. uint8_t statusLedData[9] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
  117. uint32_t statusTimer = 0;
  118.  
  119. esp8266ArtNetRDM artRDM;
  120. ESP8266WebServer webServer(80);
  121. DynamicJsonBuffer jsonBuffer;
  122. ws2812Driver pixDriver;
  123. File fsUploadFile;
  124. bool statusLedsDim = true;
  125. bool statusLedsOff = false;
  126.  
  127. pixPatterns pixFXA(0, &pixDriver);
  128. pixPatterns pixFXB(1, &pixDriver);
  129.  
  130. const char PROGMEM mainPage[] = "<!DOCTYPE html><meta content='text/html; charset=utf-8' http-equiv=Content-Type /><title>ESP8266 ArtNetNode Config</title><meta content='Matthew Tong - http://github.com/mtongnz/' name=DC.creator /><meta content=en name=DC.language /><meta content='width=device-width,initial-scale=1' name=viewport /><link href=style.css rel=stylesheet /><div id=page><div class=inner><div class=mast><div class=title>esp8266<h1>ArtNet & sACN</h1>to<h1>DMX & LED Pixels</h1></div><ul class=nav><li class=first><a href='javascript: menuClick(1)'>Device Status</a><li><a href='javascript: menuClick(2)'>WiFi</a><li><a href='javascript: menuClick(3)'>IP & Name</a><li><a href='javascript: menuClick(4)'>Port A</a>"
  131.    #ifndef ONE_PORT
  132.      "<li><a href='javascript: menuClick(5)'>Port B</a>"
  133.    #endif
  134.  "<li><a href='javascript: menuClick(6)'>Scenes</a><li><a href='javascript: menuClick(7)'>Firmware</a><li class=last><a href='javascript: reboot()'>Reboot</a></ul><div class=author><i>Design by</i> Matthew Tong</div></div><div class='main section'><div class=hide name=error><h2>Error</h2><p class=center>There was an error communicating with the device. Refresh the page and try again.</div><div class=show name=sections><h2>Fetching Data</h2><p class=center>Fetching data from device. If this message is still here in 15 seconds, try refreshing the page or clicking the menu option again.</div><div class=hide name=sections><h2>Device Status</h2><p class=left>Device Name:<p class=right name=nodeName><p class=left>MAC Address:<p class=right name=macAddress><p class=left>Wifi Status:<p class=right name=wifiStatus><p class=left>IP Address:<p class=right name=ipAddressT><p class=left>Subnet Address:<p class=right name=subAddressT><p class=left>Port A:<p class=right name=portAStatus>"
  135.    #ifndef ONE_PORT
  136.      "<p class=left>Port B:<p class=right name=portBStatus>"
  137.    #endif
  138.  "<p class=left>Scene Storage:<p class=right name=sceneStatus><p class=left>Firmware:<p class=right name=firmwareStatus></div><div class=hide name=sections><input name=save type=button value='Save Changes'/><h2>WiFi Settings</h2><p class=left>MAC Address:<p class=right name=macAddress><p class=spacer><p class=left>Wifi SSID:<p class=right><input type=text name=wifiSSID /><p class=left>Password:<p class=right><input type=text name=wifiPass /><p class=spacer><p class=left>Hotspot SSID:<p class=right><input type='text' name='hotspotSSID' /><p class=left>Password:<p class=right><input type=text name=hotspotPass /><p class=left>Start Delay:<p class=right><input name=hotspotDelay type=number min=0 max=180 class=number /> (seconds)<p class=spacer><p class=left>Stand Alone:<p class=right><input name=standAloneEnable type=checkbox value=true /><p class=right>In normal mode, the hotspot will start after <i>delay</i> seconds if the main WiFi won't connect. If no users connect, the device will reset and attempt the main WiFi again. This feature is purely for changing settings and ArtNet data is ignored.<p class=right>Stand alone mode disables the primary WiFi connection and allows ArtNet data to be received via the hotspot connection.</div><div class=hide name=sections><input name=save type=button value='Save Changes'/><h2>IP & Node Name</h2><p class='left'>Short Name:</p><p class=right><input type=text name=nodeName /><p class=left>Long Name:<p class=right><input type=text name=longName /><p class=spacer><p class=left>Enable DHCP:<p class=right><input name=dhcpEnable type=checkbox value=true /><p class=left>IP Address:<p class=right><input name=ipAddress type=number min=0 max=255 class=number /> <input name=ipAddress type=number min=0 max=255 class=number /> <input name=ipAddress type=number min=0 max=255 class=number /> <input name=ipAddress type=number min=0 max=255 class=number /><p class=left>Subnet Address:<p class=right><input name=subAddress type=number min=0 max=255 class=number /> <input name=subAddress type=number min=0 max=255 class=number /> <input name=subAddress type=number min=0 max=255 class=number /> <input name=subAddress type=number min=0 max=255 class=number /><p class=left>Gateway Address:<p class=right><input name=gwAddress type=number min=0 max=255 class=number /> <input name=gwAddress type=number min=0 max=255 class=number /> <input name=gwAddress type=number min=0 max=255 class=number /> <input name=gwAddress type=number min=0 max=255 class=number /><p class=left>Broadcast Address:<p class=right name=bcAddress><p class=center>These settings only affect the main WiFi connection. The hotspot will always have DHCP enabled and an IP of <b>2.0.0.1</b></div><div class=hide name=sections><input name=save type=button value='Save Changes'/><h2>Port A Settings</h2><p class=left>Port Type:<p class=right><select class=select name=portAmode><option value=0>DMX Output<option value=1>DMX Output with RDM<option value=2>DMX Input<option value=3>LED Pixels - WS2812</select><p class=left>Protocol:<p class=right><select class=select name=portAprot><option value=0>Artnet v4<option value=1>Artnet v4 with sACN DMX</select><p class=left>Merge Mode:<p class=right><select class=select name=portAmerge><option value=0>Merge LTP<option value=1>Merge HTP</select><p class=left>Net:<p class=right><input name=portAnet type=number min=0 max=127 class=number /><p class=left>Subnet:<p class=right><input name=portAsub type=number min=0 max=15 class=number /><p class=left>Universe:<p class=right><input type=number min=0 max=15 name=portAuni class=number /><span name=portApix> <input type=number min=0 max=15 name=portAuni class=number /> <input type=number min=0 max=15 name=portAuni class=number /> <input type=number min=0 max=15 name=portAuni class=number /></span></p><p class=left>sACN Universe:<p class=right><input type=number min=0 max=63999 name=portAsACNuni class=number /> <input type=number min=1 max=63999 name=portAsACNuni class=number /> <input type=number min=1 max=63999 name=portAsACNuni class=number /> <input type=number min=1 max=63999 name=portAsACNuni class=number /></p><span name=DmxInBcAddrA><p class=left>Broadcast Address:<p class=right><input type=number min=0 max=255 name=dmxInBroadcast class=number /> <input type=number min=0 max=255 name=dmxInBroadcast class=number /> <input type=number min=0 max=255 name=dmxInBroadcast class=number /> <input type=number min=0 max=255 name=dmxInBroadcast class=number /></p></span><span name=portApix><p class=left>Number of Pixels:<p class=right><input type=number min=0 max=680 name=portAnumPix class=number /> 680 max - 170 per universe</p><p class=left>Mode:</p><p class=right><select class=select name=portApixMode><option value=0>Pixel Mapping</option><option value=1>12 Channel FX</option></select><p class=left>Start Channel:</p><p class=right><input type=number name=portApixFXstart class=number min=1 max=501 /> FX modes only</p></span></div><div class=hide name=sections><input name=save type=button value='Save Changes'/><h2>Port B Settings</h2><p class=left>Port Type:<p class=right><select class=select name=portBmode><option value=0>DMX Output<option value=1>DMX Output with RDM<option value=3>LED Pixels - WS2812</select><p class=left>Protocol:<p class=right><select class=select name=portBprot><option value=0>Artnet v4<option value=1>Artnet v4 with sACN DMX</select><p class=left>Merge Mode:<p class=right><select class=select name=portBmerge><option value=0>Merge LTP<option value=1>Merge HTP</select><p class=left>Net:<p class=right><input name=portBnet type=number min=0 max=127 class=number /><p class=left>Subnet:<p class=right><input name=portBsub type=number min=0 max=15 class=number /><p class=left>Universe:<p class=right><input type=number min=0 max=15 name=portBuni class=number /><span name=portBpix> <input type=number min=0 max=15 name=portBuni class=number /> <input type=number min=0 max=15 name=portBuni class=number /> <input type=number min=0 max=15 name=portBuni class=number /></span></p><p class=left>sACN Universe:<p class=right><input type=number min=1 max=63999 name=portBsACNuni class=number /> <input type=number min=1 max=63999 name=portBsACNuni class=number /> <input type=number min=1 max=63999 name=portBsACNuni class=number /> <input type=number min=1 max=63999 name=portBsACNuni class=number /></p><span name=portBpix><p class=left>Number of Pixels:<p class=right><input type=number min=0 max=680 name=portBnumPix class=number /> 680 max - 170 per universe</p><p class=left>Mode:</p><p class=right><select class=select name=portBpixMode><option value=0>Pixel Mapping</option><option value=1>12 Channel FX</option></select><p class=left>Start Channel:</p><p class=right><input type=number name=portBpixFXstart class=number min=1 max=501 /> FX modes only</p></span></div><div class=hide name=sections><h2>Stored Scenes</h2><p class=center>Not yet implemented</div><div class=hide name=sections><form action=/update enctype=multipart/form-data method=POST id=firmForm><h2>Update Firmware</h2><p class=left>Firmware:<p class=right name=firmwareStatus><p class=right><input name=update type=file id=update><label for=update><svg height=17 viewBox='0 0 20 17' width=20 xmlns=http://www.w3.org/2000/svg><path d='M10 0l-5.2 4.9h3.3v5.1h3.8v-5.1h3.3l-5.2-4.9zm9.3 11.5l-3.2-2.1h-2l3.4 2.6h-3.5c-.1 0-.2.1-.2.1l-.8 2.3h-6l-.8-2.2c-.1-.1-.1-.2-.2-.2h-3.6l3.4-2.6h-2l-3.2 2.1c-.4.3-.7 1-.6 1.5l.6 3.1c.1.5.7.9 1.2.9h16.3c.6 0 1.1-.4 1.3-.9l.6-3.1c.1-.5-.2-1.2-.7-1.5z'/></svg> <span>Choose Firmware</span></label><p class=right id=uploadMsg></p><p class=right><input type=button class=submit value='Upload Now' id=fUp></div></div><div class=footer><p>Coding and hardware © 2016-2017 <a href=http://github.com/mtongnz/ >Matthew Tong</a>.<p>Released under <a href=http://www.gnu.org/licenses/ >GNU General Public License V3</a>.</div></div></div><script>var cl=0;var num=0;var err=0;var o=document.getElementsByName('sections');var s=document.getElementsByName('save');for (var i=0, e; e=s[i++];)e.addEventListener( 'click', function(){sendData();}); var u=document.getElementById('fUp');var um=document.getElementById('uploadMsg');var fileSelect=document.getElementById('update');u.addEventListener('click',function(){uploadPrep()});function uploadPrep(){if(fileSelect.files.length===0) return;u.disabled=!0;u.value='Preparing Device…';var x=new XMLHttpRequest();x.onreadystatechange=function(){if(x.readyState==XMLHttpRequest.DONE){try{var r=JSON.parse(x.response)}catch(e){var r={success:0,doUpdate:1}} if(r.success==1&&r.doUpdate==1){uploadWait()}else{um.value='<b>Update failed!</b>';u.value='Upload Now';u.disabled=!1}}};x.open('POST','/ajax',!0);x.setRequestHeader('Content-Type','application/json');x.send('{\"doUpdate\":1,\"success\":1}')} function uploadWait(){setTimeout(function(){var z=new XMLHttpRequest();z.onreadystatechange=function(){if(z.readyState==XMLHttpRequest.DONE){try{var r=JSON.parse(z.response)}catch(e){var r={success:0}} console.log('r=' + r.success); if(r.success==1){upload()}else{uploadWait()}}};z.open('POST','/ajax',!0);z.setRequestHeader('Content-Type','application/json');z.send('{\"doUpdate\":2,\"success\":1}')},1000)} var upload=function(){u.value='Uploading… 0%';var data=new FormData();data.append('update',fileSelect.files[0]);var x=new XMLHttpRequest();x.onreadystatechange=function(){if(x.readyState==4){try{var r=JSON.parse(x.response)}catch(e){var r={success:0,message:'No response from device.'}} console.log(r.success+': '+r.message);if(r.success==1){u.value=r.message;setTimeout(function(){location.reload()},15000)}else{um.value='<b>Update failed!</b> '+r.message;u.value='Upload Now';u.disabled=!1}}};x.upload.addEventListener('progress',function(e){var p=Math.ceil((e.loaded/e.total)*100);console.log('Progress: '+p+'%');if(p<100) u.value='Uploading... '+p+'%';else u.value='Upload complete. Processing…'},!1);x.open('POST','/upload',!0);x.send(data)}; function reboot() { if (err == 1) return; var r = confirm('Are you sure you want to reboot?'); if (r != true) return; o[cl].className = 'hide'; o[0].childNodes[0].innerHTML = 'Rebooting'; o[0].childNodes[1].innerHTML = 'Please wait while the device reboots. This page will refresh shortly unless you changed the IP or Wifi.'; o[0].className = 'show'; err = 0; var x = new XMLHttpRequest(); x.onreadystatechange = function(){ if(x.readyState == 4){ try { var r = JSON.parse(x.response); } catch (e){ var r = {success: 0, message: 'Unknown error: [' + x.responseText + ']'}; } if (r.success != 1) { o[0].childNodes[0].innerHTML = 'Reboot Failed'; o[0].childNodes[1].innerHTML = 'Something went wrong and the device didn\\'t respond correctly. Please try again.'; } setTimeout(function() { location.reload(); }, 5000); } }; x.open('POST', '/ajax', true); x.setRequestHeader('Content-Type', 'application/json'); x.send('{\"reboot\":1,\"success\":1}'); } function sendData(){var d={'page':num};for (var i=0, e; e=o[cl].getElementsByTagName('INPUT')[i++];){var k=e.getAttribute('name');var v=e.value;if (k in d) continue; if (k=='ipAddress' || k=='subAddress' || k=='gwAddress' || k=='portAuni' || k=='portBuni' || k=='portAsACNuni' || k=='portBsACNuni' || k=='dmxInBroadcast'){var c=[v];for (var z=1; z < 4; z++){c.push(o[cl].getElementsByTagName('INPUT')[i++].value);}d[k]=c; continue;}if (e.type==='text')d[k]=v;if (e.type==='number'){if (v=='')v=0;d[k]=v;}if (e.type==='checkbox'){if (e.checked)d[k]=1;else d[k]=0;}}for (var i=0, e; e=o[cl].getElementsByTagName('SELECT')[i++];){d[e.getAttribute('name')]=e.options[e.selectedIndex].value;}d['success']=1;var x=new XMLHttpRequest();x.onreadystatechange=function(){handleAJAX(x);};x.open('POST', '/ajax');x.setRequestHeader('Content-Type', 'application/json');x.send(JSON.stringify(d));console.log(d);} function menuClick(n){if (err==1) return; num=n; setTimeout(function(){if (cl==num || err==1) return; o[cl].className='hide'; o[0].className='show'; cl=0;}, 100); var x=new XMLHttpRequest(); x.onreadystatechange=function(){handleAJAX(x);}; x.open('POST', '/ajax'); x.setRequestHeader('Content-Type', 'application/json'); x.send(JSON.stringify({\"page\":num,\"success\":1}));}function handleAJAX(x){if (x.readyState==XMLHttpRequest.DONE ){if (x.status==200){var response=JSON.parse(x.responseText);console.log(response);if (!response.hasOwnProperty('success')){err=1; o[cl].className='hide'; document.getElementsByName('error')[0].className='show';return;}if (response['success'] !=1){err=1; o[cl].className='hide';document.getElementsByName('error')[0].getElementsByTagName('P')[0].innerHTML=response['message']; document.getElementsByName('error')[0].className='show';return;}if (response.hasOwnProperty('message')) { for (var i = 0, e; e = s[i++];) { e.value = response['message']; e.className = 'showMessage' } setTimeout(function() { for (var i = 0, e; e = s[i++];) { e.value = 'Save Changes'; e.className = '' } }, 5000); } o[cl].className='hide'; o[num].className='show'; cl=num; for (var key in response){if (response.hasOwnProperty(key)){var a=document.getElementsByName(key); if (key=='ipAddress' || key=='subAddress'){var b=document.getElementsByName(key + 'T'); for (var z=0; z < 4; z++){a[z].value=response[key][z]; if (z==0) b[0].innerHTML=''; else b[0].innerHTML=b[0].innerHTML + ' . '; b[0].innerHTML=b[0].innerHTML + response[key][z];}continue;}else if (key=='bcAddress'){for (var z=0; z < 4; z++){if (z==0) a[0].innerHTML=''; else a[0].innerHTML=a[0].innerHTML + ' . '; a[0].innerHTML=a[0].innerHTML + response[key][z];}continue;} else if (key=='gwAddress' || key=='dmxInBroadcast' || key=='portAuni' || key=='portBuni' || key=='portAsACNuni' || key=='portBsACNuni'){for(var z=0;z<4;z++){a[z].value = response[key][z];}continue}if(key=='portAmode'){var b = document.getElementsByName('portApix');var c = document.getElementsByName('DmxInBcAddrA');if(response[key] == 3) {b[0].style.display = '';b[1].style.display = '';} else {b[0].style.display = 'none';b[1].style.display = 'none';}if (response[key] == 2){c[0].style.display = '';}else{c[0].style.display = 'none';}} else if (key == 'portBmode') {var b = document.getElementsByName('portBpix');if(response[key] == 3) {b[0].style.display = '';b[1].style.display = '';} else {b[0].style.display = 'none';b[1].style.display = 'none';}}for (var z=0; z < a.length; z++){switch (a[z].nodeName){case 'P': case 'DIV': a[z].innerHTML=response[key]; break; case 'INPUT': if (a[z].type=='checkbox'){if (response[key]==1) a[z].checked=true; else a[z].checked=false;}else a[z].value=response[key]; break; case 'SELECT': for (var y=0; y < a[z].options.length; y++){if (a[z].options[y].value==response[key]){a[z].options.selectedIndex=y; break;}}break;}}}}}else{err=1; o[cl].className='hide'; document.getElementsByName('error')[0].className='show';}}}var update=document.getElementById('update');var label=update.nextElementSibling;var labelVal=label.innerHTML;update.addEventListener( 'change', function( e ){var fileName=e.target.value.split( '\\\\' ).pop(); if( fileName ) label.querySelector( 'span' ).innerHTML=fileName; else label.innerHTML=labelVal; update.blur();}); document.onkeydown=function(e){if(cl < 2 || cl > 6)return; var e = e||window.event; if (e.keyCode == 13)sendData();}; menuClick(1);</script></body></html>";
  139.  
  140. const char PROGMEM cssUploadPage[] = "<html><head><title>espArtNetNode CSS Upload</title></head><body>Select and upload your CSS file.  This will overwrite any previous uploads but you can restore the default below.<br /><br /><form method='POST' action='/style_upload' enctype='multipart/form-data'><input type='file' name='css'><input type='submit' value='Upload New CSS'></form><br /><a href='/style_delete'>Restore default CSS</a></body></html>";
  141.  
  142. const char PROGMEM css[] = ".author,.title,ul.nav a{text-align:center}.author i,.show,.title h1,ul.nav a{display:block}input,ul.nav a:hover{background-color:#DADADA}a,abbr,acronym,address,applet,b,big,blockquote,body,caption,center,cite,code,dd,del,dfn,div,dl,dt,em,fieldset,font,form,h1,h2,h3,h4,h5,h6,html,i,iframe,img,ins,kbd,label,legend,li,object,ol,p,pre,q,s,samp,small,span,strike,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,tt,u,ul,var{margin:0;padding:0;border:0;outline:0;font-size:100%;vertical-align:baseline;background:0 0}.main h2,li.last{border-bottom:1px solid #888583}body{line-height:1;background:#E4E4E4;color:#292929;color:rgba(0,0,0,.82);font:400 100% Cambria,Georgia,serif;-moz-text-shadow:0 1px 0 rgba(255,255,255,.8);}ol,ul{list-style:none}a{color:#890101;text-decoration:none;-moz-transition:.2s color linear;-webkit-transition:.2s color linear;transition:.2s color linear}a:hover{color:#DF3030}#page{padding:0}.inner{margin:0 auto;width:91%}.amp{font-family:Baskerville,Garamond,Palatino,'Palatino Linotype','Hoefler Text','Times New Roman',serif;font-style:italic;font-weight:400}.mast{float:left;width:31.875%}.title{font:semi 700 16px/1.2 Baskerville,Garamond,Palatino,'Palatino Linotype','Hoefler Text','Times New Roman',serif;padding-top:0}.title h1{font:700 20px/1.2 'Book Antiqua','Palatino Linotype',Georgia,serif;padding-top:0}.author{font:400 100% Cambria,Georgia,serif}.author i{font:400 12px Baskerville,Garamond,Palatino,'Palatino Linotype','Hoefler Text','Times New Roman',serif;letter-spacing:.05em;padding-top:.7em}.footer,.main{float:right;width:65.9375%}ul.nav{margin:1em auto 0;width:11em}ul.nav a{font:700 14px/1.2 'Book Antiqua','Palatino Linotype',Georgia,serif;letter-spacing:.1em;padding:.7em .5em;margin-bottom:0;text-transform:uppercase}input[type=button],input[type=button]:focus{background-color:#E4E4E4;color:#890101}li{border-top:1px solid #888583}.hide{display:none}.main h2{font-size:1.4em;text-align:left;margin:0 0 1em;padding:0 0 .3em}.main{position:relative}p.left{clear:left;float:left;width:20%;min-width:120px;max-width:300px;margin:0 0 .6em;padding:0;text-align:right}p.right,select{min-width:200px}p.right{overflow:auto;margin:0 0 .6em .4em;padding-left:.6em;text-align:left}p.center,p.spacer{padding:0;display:block}.footer,p.center{text-align:center}p.center{float:left;clear:both;margin:3em 0 3em 15%;width:70%}p.spacer{float:left;clear:both;margin:0;width:100%;height:20px}input{margin:0;border:0;color:#890101;outline:0;font:400 100% Cambria,Georgia,serif}input[type=text]{width:70%;min-width:200px;padding:0 5px}input[type=number]{min-width:50px;width:50px}input:focus{background-color:silver;color:#000}input[type=checkbox]{-webkit-appearance:none;background-color:#fafafa;border:1px solid #cacece;box-shadow:0 1px 2px rgba(0,0,0,.05),inset 0 -15px 10px -12px rgba(0,0,0,.05);padding:9px;border-radius:5px;display:inline-block;position:relative}input[type=checkbox]:active,input[type=checkbox]:checked:active{box-shadow:0 1px 2px rgba(0,0,0,.05),inset 0 1px 3px rgba(0,0,0,.1)}input[type=checkbox]:checked{background-color:#fafafa;border:1px solid #adb8c0;box-shadow:0 1px 2px rgba(0,0,0,.05),inset 0 -15px 10px -12px rgba(0,0,0,.05),inset 15px 10px -12px rgba(255,255,255,.1);color:#99a1a7}input[type=checkbox]:checked:after{content:'\\2714';font-size:14px;position:absolute;top:0;left:3px;color:#890101}input[type=button],input[type=file]+label{font:700 16px/1.2 'Book Antiqua','Palatino Linotype',Georgia,serif;margin:17px 0 0}input[type=button]{position:absolute;right:0;display:block;border:1px solid #adb8c0;float:right;border-radius:12px;padding:5px 20px 2px 23px;-webkit-transition-duration:.3s;transition-duration:.3s}input[type=button]:hover{background-color:#909090;color:#fff;padding:5px 62px 2px 65px}input.submit{float:left;position: relative}input.showMessage,input.showMessage:focus,input.showMessage:hover{background-color:#6F0;color:#000;padding:5px 62px 2px 65px}input[type=file]{width:.1px;height:.1px;opacity:0;overflow:hidden;position:absolute;z-index:-1}input[type=file]+label{float:left;clear:both;cursor:pointer;border:1px solid #adb8c0;border-radius:12px;padding:5px 20px 2px 23px;display:inline-block;background-color:#E4E4E4;color:#890101;overflow:hidden;-webkit-transition-duration:.3s;transition-duration:.3s}input[type=file]+label:hover,input[type=file]:focus+label{background-color:#909090;color:#fff;padding:5px 40px 2px 43px}input[type=file]+label svg{width:1em;height:1em;vertical-align:middle;fill:currentColor;margin-top:-.25em;margin-right:.25em}select{margin:0;border:0;background-color:#DADADA;color:#890101;outline:0;font:400 100% Cambria,Georgia,serif;width:50%;padding:0 5px}.footer{border-top:1px solid #888583;display:block;font-size:12px;margin-top:20px;padding:.7em 0 20px}.footer p{margin-bottom:.5em}@media (min-width:600px){.inner{min-width:600px}}@media (max-width:600px){.inner,.page{min-width:300px;width:100%;overflow-x:hidden}.footer,.main,.mast{float:left;width:100%}.mast{border-top:1px solid #888583;border-bottom:1px solid #888583}.main{margin-top:4px;width:98%}ul.nav{margin:0 auto;width:100%}ul.nav li{float:left;min-width:100px;width:33%}ul.nav a{font:12px Helvetica,Arial,sans-serif;letter-spacing:0;padding:.8em}.title,.title h1{padding:0;text-align:center}ul.nav a:focus,ul.nav a:hover{background-position:0 100%}.author{display:none}.title{border-bottom:1px solid #888583;width:100%;display:block;font:400 15px Baskerville,Garamond,Palatino,'Palatino Linotype','Hoefler Text','Times New Roman',serif}.title h1{font:600 15px Baskerville,Garamond,Palatino,'Palatino Linotype','Hoefler Text','Times New Roman',serif;display:inline}p.left,p.right{clear:both;float:left;margin-right:1em}li,li.first,li.last{border:0}p.left{width:100%;text-align:left;margin-left:.4em;font-weight:600}p.right{margin-left:1em;width:100%}p.center{margin:1em 0;width:100%}p.spacer{display:none}input[type=text],select{width:85%;}@media (min-width:1300px){.page{width:1300px}}";
  143. const char PROGMEM typeHTML[] = "text/html";
  144. const char PROGMEM typeCSS[] = "text/css";
  145.  
  146. char wifiStatus[60] = "";
  147. bool isHotspot = false;
  148. uint32_t nextNodeReport = 0;
  149. char nodeError[ARTNET_NODE_REPORT_LENGTH] = "";
  150. bool nodeErrorShowing = 1;
  151. uint32_t nodeErrorTimeout = 0;
  152. bool pixDone = true;
  153. bool newDmxIn = false;
  154. bool doReboot = false;
  155. byte* dataIn;
  156.  
  157. void setup(void) {
  158.   //pinMode(4, OUTPUT);
  159.   //digitalWrite(4, LOW);
  160.  
  161.   // Make direction input to avoid boot garbage being sent out
  162.   pinMode(DMX_DIR_A, OUTPUT);
  163.   digitalWrite(DMX_DIR_A, LOW);
  164.   #ifndef ONE_PORT
  165.     pinMode(DMX_DIR_B, OUTPUT);
  166.     digitalWrite(DMX_DIR_B, LOW);
  167.   #endif
  168.  
  169.   #ifndef ESP_01
  170.     pinMode(STATUS_LED_PIN, OUTPUT);
  171.     digitalWrite(STATUS_LED_PIN, LOW);
  172.     delay(1);
  173.     setStatusLed(STATUS_LED_S, PINK);
  174.     doStatusLedOutput();
  175.   #endif
  176.  
  177.   wifi_set_sleep_type(NONE_SLEEP_T);
  178.   bool resetDefaults = false;
  179.  
  180.   #ifdef SETTINGS_RESET
  181.     pinMode(SETTINGS_RESET, INPUT);
  182.  
  183.     delay(5);
  184.     // button pressed = low reading
  185.     if (!digitalRead(SETTINGS_RESET)) {
  186.       delay(50);
  187.       if (!digitalRead(SETTINGS_RESET))
  188.         resetDefaults = true;
  189.     }
  190.   #endif
  191.  
  192.   // Start EEPROM
  193.   EEPROM.begin(512);
  194.  
  195.   // Start SPIFFS file system
  196.   SPIFFS.begin();
  197.  
  198.   // Check if SPIFFS formatted
  199.   if (SPIFFS.exists("/formatted.txt")) {
  200.     SPIFFS.format();
  201.    
  202.     File f = SPIFFS.open("/formatted.txt", "w");
  203.     f.print("Formatted");
  204.     f.close();
  205.   }
  206.  
  207.   // Load our saved values or store defaults
  208.   if (!resetDefaults)
  209.     eepromLoad();
  210.  
  211.   // Store our counters for resetting defaults
  212.   if (resetInfo.reason != REASON_DEFAULT_RST && resetInfo.reason != REASON_EXT_SYS_RST && resetInfo.reason != REASON_SOFT_RESTART)
  213.    deviceSettings.wdtCounter++;
  214.   else
  215.     deviceSettings.resetCounter++;
  216.  
  217.   // Store values
  218.   eepromSave();
  219.  
  220.   // Start wifi
  221.   wifiStart();
  222.  
  223.   // Start web server
  224.   webStart();
  225.  
  226.  
  227.   // Don't start our Artnet or DMX in firmware update mode or after multiple WDT resets
  228.   if (!deviceSettings.doFirmwareUpdate && deviceSettings.wdtCounter <= 3) {
  229.  
  230.    // We only allow 1 DMX input - and RDM can't run alongside DMX in
  231.    if (deviceSettings.portAmode == TYPE_DMX_IN && deviceSettings.portBmode == TYPE_RDM_OUT)
  232.      deviceSettings.portBmode = TYPE_DMX_OUT;
  233.    
  234.     // Setup Artnet Ports & Callbacks
  235.    artStart();
  236.  
  237.     // Don't open any ports for a bit to let the ESP spill it's garbage to serial
  238.     while (millis() < 3500)
  239.      yield();
  240.    
  241.    // Port Setup
  242.    portSetup();
  243.  
  244.  } else
  245.    deviceSettings.doFirmwareUpdate = false;
  246.  
  247.  delay(10);
  248. }
  249.  
  250. void loop(void){
  251.  // If the device lasts for 6 seconds, clear our reset timers
  252.  if (deviceSettings.resetCounter != 0 && millis() > 6000) {
  253.     deviceSettings.resetCounter = 0;
  254.     deviceSettings.wdtCounter = 0;
  255.     eepromSave();
  256.   }
  257.  
  258.   webServer.handleClient();
  259.  
  260.   // Get the node details and handle Artnet
  261.   doNodeReport();
  262.   artRDM.handler();
  263.  
  264.   yield();
  265.  
  266.   // DMX handlers
  267.   dmxA.handler();
  268.   #ifndef ONE_PORT
  269.     dmxB.handler();
  270.   #endif
  271.  
  272.   // Do Pixel FX on port A
  273.   if (deviceSettings.portAmode == TYPE_WS2812 && deviceSettings.portApixMode != FX_MODE_PIXEL_MAP) {
  274.    if (pixFXA.Update())
  275.      pixDone = 0;
  276.   }
  277.  
  278.   // Do Pixel FX on port B
  279.   #ifndef ONE_PORT
  280.     if (deviceSettings.portBmode == TYPE_WS2812 && deviceSettings.portBpixMode != FX_MODE_PIXEL_MAP) {
  281.      if (pixFXB.Update())
  282.        pixDone = 0;
  283.     }
  284.   #endif
  285.  
  286.   // Do pixel string output
  287.   if (!pixDone)
  288.     pixDone = pixDriver.show();
  289.  
  290.   // Handle received DMX
  291.   if (newDmxIn) {
  292.     uint8_t g, p, n;
  293.    
  294.     newDmxIn = false;
  295.  
  296.     g = portA[0];
  297.     p = portA[1];
  298.    
  299.     IPAddress bc = deviceSettings.dmxInBroadcast;
  300.     artRDM.sendDMX(g, p, bc, dataIn, 512);
  301.  
  302.     #ifndef ESP_01
  303.       setStatusLed(STATUS_LED_A, CYAN);
  304.     #endif
  305.   }
  306.  
  307.   // Handle rebooting the system
  308.   if (doReboot) {
  309.     char c[ARTNET_NODE_REPORT_LENGTH] = "Device rebooting...";
  310.     artRDM.setNodeReport(c, ARTNET_RC_POWER_OK);
  311.     artRDM.artPollReply();
  312.    
  313.     // Ensure all web data is sent before we reboot
  314.     uint32_t n = millis() + 1000;
  315.     while (millis() < n)
  316.      webServer.handleClient();
  317.  
  318.    ESP.restart();
  319.  }
  320.  
  321.  #ifdef STATUS_LED_PIN
  322.    // Output status to LEDs once per second
  323.    if (statusTimer < millis()) {
  324.  
  325.      // Flash our main status LED
  326.      if ((statusTimer % 2000) > 1000)
  327.         setStatusLed(STATUS_LED_S, BLACK);
  328.       else if (nodeError[0] != '\0')
  329.         setStatusLed(STATUS_LED_S, RED);
  330.       else
  331.         setStatusLed(STATUS_LED_S, GREEN);
  332.  
  333.       doStatusLedOutput();
  334.       statusTimer = millis() + 1000;
  335.     }
  336.   #endif
  337. }
  338.  
  339. void dmxHandle(uint8_t group, uint8_t port, uint16_t numChans, bool syncEnabled) {
  340.   if (portA[0] == group) {
  341.     if (deviceSettings.portAmode == TYPE_WS2812) {
  342.      
  343.       #ifndef ESP_01
  344.         setStatusLed(STATUS_LED_A, GREEN);
  345.       #endif
  346.      
  347.       if (deviceSettings.portApixMode == FX_MODE_PIXEL_MAP) {
  348.         if (numChans > 510)
  349.           numChans = 510;
  350.        
  351.         // Copy DMX data to the pixels buffer
  352.         pixDriver.setBuffer(0, port * 510, artRDM.getDMX(group, port), numChans);
  353.        
  354.         // Output to pixel strip
  355.         if (!syncEnabled)
  356.           pixDone = false;
  357.  
  358.         return;
  359.  
  360.       // FX 12 Mode
  361.       } else if (port == portA[1]) {
  362.         byte* a = artRDM.getDMX(group, port);
  363.         uint16_t s = deviceSettings.portApixFXstart - 1;
  364.        
  365.         pixFXA.Intensity = a[s + 0];
  366.         pixFXA.setFX(a[s + 1]);
  367.         pixFXA.setSpeed(a[s + 2]);
  368.         pixFXA.Pos = a[s + 3];
  369.         pixFXA.Size = a[s + 4];
  370.        
  371.         pixFXA.setColour1((a[s + 5] << 16) | (a[s + 6] << 8) | a[s + 7]);
  372.        pixFXA.setColour2((a[s + 8] << 16) | (a[s + 9] << 8) | a[s + 10]);
  373.        pixFXA.Size1 = a[s + 11];
  374.        //pixFXA.Fade = a[s + 12];
  375.  
  376.        pixFXA.NewData = 1;
  377.          
  378.      }
  379.  
  380.    // DMX modes
  381.    } else if (deviceSettings.portAmode != TYPE_DMX_IN && port == portA[1]) {
  382.      dmxA.chanUpdate(numChans);
  383.      
  384.      #ifndef ESP_01
  385.        setStatusLed(STATUS_LED_A, BLUE);
  386.      #endif
  387.    }
  388.      
  389.  
  390.  #ifndef ONE_PORT
  391.  } else if (portB[0] == group) {
  392.    if (deviceSettings.portBmode == TYPE_WS2812) {
  393.      setStatusLed(STATUS_LED_B, GREEN);
  394.      
  395.      if (deviceSettings.portBpixMode == FX_MODE_PIXEL_MAP) {
  396.        if (numChans > 510)
  397.           numChans = 510;
  398.        
  399.         // Copy DMX data to the pixels buffer
  400.         pixDriver.setBuffer(1, port * 510, artRDM.getDMX(group, port), numChans);
  401.        
  402.         // Output to pixel strip
  403.         if (!syncEnabled)
  404.           pixDone = false;
  405.  
  406.         return;
  407.  
  408.       // FX 12 mode
  409.       } else if (port == portB[1]) {
  410.         byte* a = artRDM.getDMX(group, port);
  411.         uint16_t s = deviceSettings.portBpixFXstart - 1;
  412.        
  413.         pixFXB.Intensity = a[s + 0];
  414.         pixFXB.setFX(a[s + 1]);
  415.         pixFXB.setSpeed(a[s + 2]);
  416.         pixFXB.Pos = a[s + 3];
  417.         pixFXB.Size = a[s + 4];
  418.         pixFXB.setColour1((a[s + 5] << 16) | (a[s + 6] << 8) | a[s + 7]);
  419.        pixFXB.setColour2((a[s + 8] << 16) | (a[s + 9] << 8) | a[s + 10]);
  420.        pixFXB.Size1 = a[s + 11];
  421.        //pixFXB.Fade = a[s + 12];
  422.  
  423.        pixFXB.NewData = 1;
  424.      }
  425.    } else if (deviceSettings.portBmode != TYPE_DMX_IN && port == portB[1]) {
  426.      dmxB.chanUpdate(numChans);
  427.      setStatusLed(STATUS_LED_B, BLUE);
  428.    }
  429.  #endif
  430.  }
  431.  
  432. }
  433.  
  434. void syncHandle() {
  435.  if (deviceSettings.portAmode == TYPE_WS2812) {
  436.    rdmPause(1);
  437.    pixDone = pixDriver.show();
  438.    rdmPause(0);
  439.  } else if (deviceSettings.portAmode != TYPE_DMX_IN)
  440.    dmxA.unPause();
  441.  
  442.  #ifndef ONE_PORT
  443.    if (deviceSettings.portBmode == TYPE_WS2812) {
  444.      rdmPause(1);
  445.      pixDone = pixDriver.show();
  446.      rdmPause(0);
  447.    } else if (deviceSettings.portBmode != TYPE_DMX_IN)
  448.      dmxB.unPause();
  449.  #endif
  450. }
  451.  
  452. void ipHandle() {
  453.  if (artRDM.getDHCP()) {
  454.    deviceSettings.gateway = INADDR_NONE;
  455.    
  456.    deviceSettings.dhcpEnable = 1;
  457.    doReboot = true;
  458.    /*
  459.    // Re-enable DHCP
  460.    WiFi.begin(deviceSettings.wifiSSID, deviceSettings.wifiPass);
  461.  
  462.    // Wait for an IP
  463.    while (WiFi.status() != WL_CONNECTED)
  464.      yield();
  465.    
  466.    // Save settings to struct
  467.    deviceSettings.ip = WiFi.localIP();
  468.    deviceSettings.subnet = WiFi.subnetMask();
  469.    deviceSettings.broadcast = {~deviceSettings.subnet[0] | (deviceSettings.ip[0] & deviceSettings.subnet[0]), ~deviceSettings.subnet[1] | (deviceSettings.ip[1] & deviceSettings.subnet[1]), ~deviceSettings.subnet[2] | (deviceSettings.ip[2] & deviceSettings.subnet[2]), ~deviceSettings.subnet[3] | (deviceSettings.ip[3] & deviceSettings.subnet[3])};
  470.  
  471.    // Pass IP to artRDM
  472.    artRDM.setIP(deviceSettings.ip, deviceSettings.subnet);
  473.    */
  474.  
  475.  } else {
  476.    deviceSettings.ip = artRDM.getIP();
  477.    deviceSettings.subnet = artRDM.getSubnetMask();
  478.    deviceSettings.gateway = deviceSettings.ip;
  479.    deviceSettings.gateway[3] = 1;
  480.    deviceSettings.broadcast = {~deviceSettings.subnet[0] | (deviceSettings.ip[0] & deviceSettings.subnet[0]), ~deviceSettings.subnet[1] | (deviceSettings.ip[1] & deviceSettings.subnet[1]), ~deviceSettings.subnet[2] | (deviceSettings.ip[2] & deviceSettings.subnet[2]), ~deviceSettings.subnet[3] | (deviceSettings.ip[3] & deviceSettings.subnet[3])};
  481.    deviceSettings.dhcpEnable = 0;
  482.    
  483.    doReboot = true;
  484.    
  485.    //WiFi.config(deviceSettings.ip,deviceSettings.ip,deviceSettings.ip,deviceSettings.subnet);
  486.  }
  487.  
  488.  // Store everything to EEPROM
  489.  eepromSave();
  490. }
  491.  
  492. void addressHandle() {
  493.  memcpy(&deviceSettings.nodeName, artRDM.getShortName(), ARTNET_SHORT_NAME_LENGTH);
  494.  memcpy(&deviceSettings.longName, artRDM.getLongName(), ARTNET_LONG_NAME_LENGTH);
  495.  
  496.  deviceSettings.portAnet = artRDM.getNet(portA[0]);
  497.  deviceSettings.portAsub = artRDM.getSubNet(portA[0]);
  498.  deviceSettings.portAuni[0] = artRDM.getUni(portA[0], portA[1]);
  499.  deviceSettings.portAmerge = artRDM.getMerge(portA[0], portA[1]);
  500.  
  501.  if (artRDM.getE131(portA[0], portA[1]))
  502.    deviceSettings.portAprot = PROT_ARTNET_SACN;
  503.  else
  504.    deviceSettings.portAprot = PROT_ARTNET;
  505.  
  506.  
  507.  #ifndef ONE_PORT
  508.    deviceSettings.portBnet = artRDM.getNet(portB[0]);
  509.    deviceSettings.portBsub = artRDM.getSubNet(portB[0]);
  510.    deviceSettings.portBuni[0] = artRDM.getUni(portB[0], portB[1]);
  511.    deviceSettings.portBmerge = artRDM.getMerge(portB[0], portB[1]);
  512.    
  513.    if (artRDM.getE131(portB[0], portB[1]))
  514.      deviceSettings.portBprot = PROT_ARTNET_SACN;
  515.    else
  516.      deviceSettings.portBprot = PROT_ARTNET;
  517.  #endif
  518.  
  519.  // Store everything to EEPROM
  520.  eepromSave();
  521. }
  522.  
  523. void rdmHandle(uint8_t group, uint8_t port, rdm_data* c) {
  524.  if (portA[0] == group && portA[1] == port)
  525.    dmxA.rdmSendCommand(c);
  526.  
  527.  #ifndef ONE_PORT
  528.    else if (portB[0] == group && portB[1] == port)
  529.      dmxB.rdmSendCommand(c);
  530.  #endif
  531. }
  532.  
  533. void rdmReceivedA(rdm_data* c) {
  534.  artRDM.rdmResponse(c, portA[0], portA[1]);
  535. }
  536.  
  537. void sendTodA() {
  538.  artRDM.artTODData(portA[0], portA[1], dmxA.todMan(), dmxA.todDev(), dmxA.todCount(), dmxA.todStatus());
  539. }
  540.  
  541. #ifndef ONE_PORT
  542. void rdmReceivedB(rdm_data* c) {
  543.  artRDM.rdmResponse(c, portB[0], portB[1]);
  544. }
  545.  
  546. void sendTodB() {
  547.  artRDM.artTODData(portB[0], portB[1], dmxB.todMan(), dmxB.todDev(), dmxB.todCount(), dmxB.todStatus());
  548. }
  549. #endif
  550.  
  551. void todRequest(uint8_t group, uint8_t port) {
  552.  if (portA[0] == group && portA[1] == port)
  553.    sendTodA();
  554.  
  555.  #ifndef ONE_PORT
  556.    else if (portB[0] == group && portB[1] == port)
  557.      sendTodB();
  558.  #endif
  559. }
  560.  
  561. void todFlush(uint8_t group, uint8_t port) {
  562.  if (portA[0] == group && portA[1] == port)
  563.    dmxA.rdmDiscovery();
  564.  
  565.  #ifndef ONE_PORT
  566.    else if (portB[0] == group && portB[1] == port)
  567.      dmxB.rdmDiscovery();
  568.  #endif
  569. }
  570.  
  571. void dmxIn(uint16_t num) {
  572.  // Double buffer switch
  573.  byte* tmp = dataIn;
  574.  dataIn = dmxA.getChans();
  575.  dmxA.setBuffer(tmp);
  576.  
  577.  newDmxIn = true;
  578. }
  579.  
  580. void doStatusLedOutput() {
  581.  uint8_t a[9] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
  582.  
  583.  if (!statusLedsOff) {
  584.    if (statusLedsDim) {
  585.      for (uint8_t x = 0; x < 9; x++)
  586.        a[x] = statusLedData[x] & STATUS_DIM;
  587.    } else {
  588.      for (uint8_t x = 0; x < 9; x++)
  589.        a[x] = statusLedData[x];
  590.    }
  591.  }
  592.  
  593.  #ifdef STATUS_LED_MODE_APA106
  594.    pixDriver.doAPA106(&a[0], STATUS_LED_PIN, 9);
  595.  #endif
  596.  
  597.  #ifdef STATUS_LED_MODE_WS2812
  598.    pixDriver.doPixel(&a[0], STATUS_LED_PIN, 9);
  599.  #endif
  600.  
  601.  // Tint LEDs red slightly - they'll be changed back before being displayed if no errors
  602.  for (uint8_t x = 1; x < 9; x += 3)
  603.    statusLedData[x] = 125;
  604. }
  605. void setStatusLed(uint8_t num, uint32_t col) {
  606.  memcpy(&statusLedData[num*3], &col, 3);
  607. }
captcha