diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..080e70d
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,10 @@
+{
+    // See http://go.microsoft.com/fwlink/?LinkId=827846
+    // for the documentation about the extensions.json format
+    "recommendations": [
+        "platformio.platformio-ide"
+    ],
+    "unwantedRecommendations": [
+        "ms-vscode.cpptools-extension-pack"
+    ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..cad7657
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+    "cmake.configureOnOpen": false
+}
\ No newline at end of file
diff --git a/include/README b/include/README
new file mode 100644
index 0000000..194dcd4
--- /dev/null
+++ b/include/README
@@ -0,0 +1,39 @@
+
+This directory is intended for project header files.
+
+A header file is a file containing C declarations and macro definitions
+to be shared between several project source files. You request the use of a
+header file in your project source file (C, C++, etc) located in `src` folder
+by including it, with the C preprocessing directive `#include'.
+
+```src/main.c
+
+#include "header.h"
+
+int main (void)
+{
+ ...
+}
+```
+
+Including a header file produces the same results as copying the header file
+into each source file that needs it. Such copying would be time-consuming
+and error-prone. With a header file, the related declarations appear
+in only one place. If they need to be changed, they can be changed in one
+place, and programs that include the header file will automatically use the
+new version when next recompiled. The header file eliminates the labor of
+finding and changing all the copies as well as the risk that a failure to
+find one copy will result in inconsistencies within a program.
+
+In C, the usual convention is to give header files names that end with `.h'.
+It is most portable to use only letters, digits, dashes, and underscores in
+header file names, and at most one dot.
+
+Read more about using header files in official GCC documentation:
+
+* Include Syntax
+* Include Operation
+* Once-Only Headers
+* Computed Includes
+
+https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
diff --git a/lib/README b/lib/README
new file mode 100644
index 0000000..6debab1
--- /dev/null
+++ b/lib/README
@@ -0,0 +1,46 @@
+
+This directory is intended for project specific (private) libraries.
+PlatformIO will compile them to static libraries and link into executable file.
+
+The source code of each library should be placed in a an own separate directory
+("lib/your_library_name/[here are source files]").
+
+For example, see a structure of the following two libraries `Foo` and `Bar`:
+
+|--lib
+|  |
+|  |--Bar
+|  |  |--docs
+|  |  |--examples
+|  |  |--src
+|  |     |- Bar.c
+|  |     |- Bar.h
+|  |  |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
+|  |
+|  |--Foo
+|  |  |- Foo.c
+|  |  |- Foo.h
+|  |
+|  |- README --> THIS FILE
+|
+|- platformio.ini
+|--src
+   |- main.c
+
+and a contents of `src/main.c`:
+```
+#include <Foo.h>
+#include <Bar.h>
+
+int main (void)
+{
+  ...
+}
+
+```
+
+PlatformIO Library Dependency Finder will find automatically dependent
+libraries scanning project source files.
+
+More information about PlatformIO Library Dependency Finder
+- https://docs.platformio.org/page/librarymanager/ldf.html
diff --git a/platformio.ini b/platformio.ini
new file mode 100644
index 0000000..1a4eef4
--- /dev/null
+++ b/platformio.ini
@@ -0,0 +1,29 @@
+; PlatformIO Project Configuration File
+;
+;   Build options: build flags, source filter
+;   Upload options: custom upload port, speed and extra flags
+;   Library options: dependencies, extra library storages
+;   Advanced options: extra scripting
+;
+; Please visit documentation for the other options and examples
+; https://docs.platformio.org/page/projectconf.html
+[platformio]
+default_envs = nodemcuv2
+
+[env:huzzah]
+platform = https://github.com/platformio/platform-espressif8266.git
+board = huzzah
+framework = arduino
+lib_deps = tzapu/WiFiManager@^0.16.0
+upload_speed = 115200
+monitor_speed = 115200
+
+[env:nodemcuv2]
+platform = https://github.com/platformio/platform-espressif8266.git
+board = nodemcuv2
+framework = arduino
+lib_deps = tzapu/WiFiManager@^0.16.0
+upload_speed = 115200
+monitor_speed = 115200
+upload_port = COM5
+monitor_port = COM5
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..1e66f4a
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,543 @@
+#include <Arduino.h>
+#include <ESP8266WiFi.h>
+#include <WiFiUdp.h>
+#include <stdlib.h>
+
+#include <DNSServer.h>
+#include <ESP8266WebServer.h>
+#include <WiFiManager.h>
+
+// The GPIO pin numbers for the three colors. Make sure these match your wiring/schematic.
+#define GPIO_RED 13
+#define GPIO_GREEN 12
+#define GPIO_BLUE 15
+
+// The target ID of this device on the network if ID_SWITCHED is false
+// Commands sent to target ID 0 (zero) will be executed by ALL targets
+#define TARGET_ID_FIXED 1
+
+// If a 4-position dip switch is present and tied to 4 GPIO pins, set ID_SWITCHED to true and put the pin numbers in the ID_BIT_? settings
+#define ID_SWITCHED true
+#define ID_BIT_0 16
+#define ID_BIT_1 14
+#define ID_BIT_2 4
+#define ID_BIT_3 5
+
+// Used to turn debug serial output on/off
+#define DEBUG false
+
+// UDP Command values
+#define CMD_OFF         0x00
+#define CMD_SETLEVELS   0x01
+#define CMD_AUTOPATTERN 0x02
+#define CMD_AUTODISABLE 0x03
+
+// Auto-cycler state conditions
+#define AUTO_DISABLED   0x00
+#define AUTO_ACTIVE     0x01
+
+// Maximum analog output level
+#define MAX_ANALOG 1023
+
+// ID Numbers for the UDP Packet filter
+#define PACKET_FILTER_1 4039196302
+#define PACKET_FILTER_2 3194769291
+
+// Set up this reference so we can put the huge array definition at the end
+extern const uint16_t gamma10[];
+
+// Variables for tracking blue LED blink state during initialization
+volatile bool blueInitState = false;
+volatile unsigned long blueToggleCount;
+
+// Struct for storing color value sets
+// Colors are obvious. restDuration is how long to rest on this color.
+struct colorTriplet {
+   short red;
+   short green;
+   short blue;
+   unsigned long restDuration;
+
+   colorTriplet() {
+      red = 0;
+      green = 0;
+      blue = 0;
+      restDuration = 0;
+   }
+};
+
+// This gets dynamically reset in setup() from the DIP switch or the #define at the top of the sketch so make sure this is set to 0 (zero)
+byte myTargetID = 0;
+
+// Variables for keeping the state of automatic color switching
+byte autoMode = AUTO_DISABLED;
+bool autoActive = false;
+
+// For storing automatic color values
+#define MAX_AUTO_COLORS 35
+unsigned int rampDuration;
+byte numAutoColors = 0;
+struct colorTriplet autoColors[MAX_AUTO_COLORS];
+byte autoColorTargetIndex = 0;
+short redStatic = 0;
+short greenStatic = 0;
+short blueStatic = 0;
+bool resting = false;
+
+// Debugging crap
+String rampValues[200] = {""};
+int rampValuesIndex = 0;
+
+// We're going to listen on port 6565
+WiFiUDP listener;
+
+// Variables to track message IDs so we don't process duplicate messages
+unsigned int lastMessageID = 0;
+unsigned int curMessageID = 0;
+
+// Reset function
+void(* resetFunc) (void) = 0;
+
+void inline blueBlinkISR(void) {
+   if ( blueToggleCount < 60 ) {
+      blueInitState = !blueInitState;
+      analogWrite(GPIO_BLUE, (blueInitState) ? MAX_ANALOG : 0);
+   }
+   blueToggleCount++;
+   if ( DEBUG ) {
+      Serial.print("Init State: bIS=");
+      Serial.print(blueInitState);
+      Serial.print(" bTC=");
+      Serial.println(blueToggleCount);
+   }
+   if ( blueToggleCount > 600 ) {
+      timer0_detachInterrupt();
+      analogWrite(GPIO_BLUE, 0);
+      ESP.restart();
+   }
+   timer0_write(ESP.getCycleCount() + 40000000);
+}
+
+// Set up our initial states and WiFi
+void setup() {
+   // Set the three color PWM pins to output
+   pinMode(GPIO_RED, OUTPUT);
+   pinMode(GPIO_GREEN, OUTPUT);
+   pinMode(GPIO_BLUE, OUTPUT);
+
+   // If ID_SWITCHED is true then set myTargetID from the dip switch, otherwise set it from TARGET_ID_FIXED
+   if ( ID_SWITCHED ) {
+      pinMode(ID_BIT_0, INPUT_PULLUP);
+      pinMode(ID_BIT_1, INPUT_PULLUP);
+      pinMode(ID_BIT_2, INPUT_PULLUP);
+      pinMode(ID_BIT_3, INPUT_PULLUP);
+      bitWrite(myTargetID, 0, ((digitalRead(ID_BIT_0) == HIGH) ? 1 : 0));
+      bitWrite(myTargetID, 1, ((digitalRead(ID_BIT_1) == HIGH) ? 1 : 0));
+      bitWrite(myTargetID, 2, ((digitalRead(ID_BIT_2) == HIGH) ? 1 : 0));
+      bitWrite(myTargetID, 3, ((digitalRead(ID_BIT_3) == HIGH) ? 1 : 0));
+      //pinMode(ID_BIT_1, INPUT);
+   } else {
+      myTargetID = TARGET_ID_FIXED;
+   }
+
+   // Start up the serial console output
+   Serial.begin(115200);
+   Serial.println("--- Serial Output Started ---");
+   delay(10);
+
+   WiFiManager wifiManager;
+   //wifiManager.resetSettings();
+   noInterrupts();
+   timer0_isr_init();
+   timer0_attachInterrupt(blueBlinkISR);
+   timer0_write(ESP.getCycleCount() + 1000);
+   interrupts();
+   wifiManager.autoConnect("JaJLEDController");
+
+   // Show our ID!
+   Serial.println();
+   Serial.print("My target ID: ");
+   Serial.println(myTargetID);
+
+   // Turn off blue LED and disable interrupt
+   timer0_detachInterrupt();
+   analogWrite(GPIO_BLUE, 0);
+
+   Serial.println("");
+   Serial.println("WiFi connected");
+   Serial.println("IP address: ");
+   Serial.println(WiFi.localIP());
+
+   // Start listening for packets on port 6565 (yes, this should probably be defined at the top)
+   listener.begin(6565);
+}
+
+bool processMessage() {
+   struct colorTriplet colorBlank;
+   byte command;
+   uint32 messageTargetID = 0;
+
+   // Read in the packet data to buff (up to 1024 bytes which is way more than we would ever need)
+   char buff[1024];
+   listener.read(buff, 1024);
+   listener.flush();
+
+   // Check for the Packet Filter ID values and skip packet if not matched
+   unsigned int filter1, filter2;
+   memcpy(&filter1, (char*)buff, 4);
+   memcpy(&filter2, (char*)buff + 4, 4);
+   if ( (filter1 != PACKET_FILTER_1) || (filter2 != PACKET_FILTER_2) ) {
+      if ( DEBUG ) {
+         Serial.println("Filter Fail: " + String(filter1) + ", " + String(filter2));
+      }
+      return false;
+   }
+
+   // Parse out the messageID field
+   memcpy(&curMessageID, (char*)buff + 8, 4);
+   // If we've seen the message before, skip it, otherwise update lastMessageID
+   if ( curMessageID == lastMessageID ) return false;
+   lastMessageID = curMessageID;
+
+   // Parse out the command and target ID fields
+   memcpy(&command, (char*)buff + 12, 1);
+
+   memcpy(&messageTargetID, (char*)buff + 13, 4);
+
+   // If this packet isn't destined for ID=0 (all targets) or ID=TARGET_ID (our ID) then stop processing
+   if ( (myTargetID != 0 ) && (messageTargetID != 0) && (messageTargetID != myTargetID) ) {
+      if ( DEBUG ) {
+         Serial.println("Got packet for different ID! Mine=" + String(myTargetID) + " Target=" + String(messageTargetID));
+      }
+      return false;
+   }
+
+   if ( DEBUG ) Serial.println("Got UDP packet!");
+
+   // Clear out the global autoColors[] array
+   memset(autoColors, 0, sizeof(autoColors));
+
+   // If we get a CMD_SETLEVELS then disable autoMode, set the new static values and ramp to that color
+   if ( command == CMD_SETLEVELS ) {
+      autoMode = AUTO_DISABLED;
+      numAutoColors = 1;
+      autoColorTargetIndex = 0;
+      memcpy(&rampDuration, (char*)buff + 17, 4);
+      memcpy(&autoColors[0].red, (char*)buff + 21, 1);
+      memcpy(&autoColors[0].green, (char*)buff + 22, 1);
+      memcpy(&autoColors[0].blue, (char*)buff + 23, 1);
+      autoColors[0].red = map(autoColors[0].red, 0, 255, 0, 1023);
+      autoColors[0].green = map(autoColors[0].green, 0, 255, 0, 1023);
+      autoColors[0].blue = map(autoColors[0].blue, 0, 255, 0, 1023);
+      redStatic = autoColors[0].red;
+      greenStatic = autoColors[0].green;
+      blueStatic = autoColors[0].blue;
+      if ( DEBUG ) {
+         String debugOutput = "Setting Levels: ";
+         debugOutput += String(redStatic) + ", ";
+         debugOutput += String(greenStatic) + ", ";
+         debugOutput += String(blueStatic) + ", ";
+         debugOutput += String(rampDuration);
+         Serial.println(debugOutput);
+      }
+      return true;
+   }
+
+   // If we get a CMD_AUTOPATTERN then build up the autoColors array and set autoMode to AUTO_ACTIVE
+   if ( command == CMD_AUTOPATTERN ) {
+      String debugOutput = "";
+      autoMode = AUTO_ACTIVE;
+      autoColorTargetIndex = 0;
+      memcpy(&rampDuration, (char*)buff + 17, 4);
+      memcpy(&numAutoColors, (char*)buff + 21, 1);
+      if ( numAutoColors > MAX_AUTO_COLORS ) numAutoColors = MAX_AUTO_COLORS;
+      if ( DEBUG ) debugOutput += "New AutoPattern " + String(numAutoColors) + ": ";
+      for ( int i=0; i<numAutoColors; i++ ) {
+         memcpy(&autoColors[i].red, (char*)buff + 22 + (i*7), 1);
+         memcpy(&autoColors[i].green, (char*)buff + 23 + (i*7), 1);
+         memcpy(&autoColors[i].blue, (char*)buff + 24 + (i*7), 1);
+         memcpy(&autoColors[i].restDuration, (char*)buff + 25 + (i*7), 4);
+         autoColors[i].red = map(autoColors[i].red, 0, 255, 0, 1023);
+         autoColors[i].green = map(autoColors[i].green, 0, 255, 0, 1023);
+         autoColors[i].blue = map(autoColors[i].blue, 0, 255, 0, 1023);
+         if ( DEBUG ) {
+            debugOutput += String(i) + "=[";
+            debugOutput += String(autoColors[i].red) + ", ";
+            debugOutput += String(autoColors[i].green) + ", ";
+            debugOutput += String(autoColors[i].blue) + ", ";
+            debugOutput += String(autoColors[i].restDuration) + "], ";
+         }
+      }
+      if ( DEBUG ) {
+         debugOutput += "Ramp=" + String(rampDuration);
+         Serial.println(debugOutput);
+      }
+      return true;
+   }
+
+   // If we get a CMD_AUTODISABLE then disable autoMode and ramp back to the last static color levels
+   if ( command == CMD_AUTODISABLE ) {
+      autoMode = AUTO_DISABLED;
+      numAutoColors = 1;
+      autoColorTargetIndex = 0;
+      autoColors[0].red = redStatic;
+      autoColors[0].green = greenStatic;
+      autoColors[0].blue = blueStatic;
+      rampDuration = 1000;
+      if ( DEBUG ) {
+         String debugOutput = "Resetting Levels to static: ";
+         debugOutput += String(redStatic) + ", ";
+         debugOutput += String(greenStatic) + ", ";
+         debugOutput += String(blueStatic) + ", ";
+         debugOutput += "Ramp=" + String(rampDuration);
+         Serial.println(debugOutput);
+      }
+      return true;
+   }
+
+   // If we get a CMD_OFF then set the levels to zero and ensure autoMode is disabled
+   // The rampDuration is set to the same value as the step value during ramping (yes, this should probably be a constant or a #define)
+   if ( command == CMD_OFF ) {
+      autoMode = AUTO_DISABLED;
+      numAutoColors = 1;
+      autoColorTargetIndex = 0;
+      rampDuration = 10;
+      autoColors[0].restDuration = 10000;
+      redStatic = 0;
+      greenStatic = 0;
+      blueStatic = 0;
+      if ( DEBUG ) Serial.println("Shutting off LEDs");
+      return true;
+   }
+
+   return false;
+}
+
+void loop() {
+   static short redTarget = 0, greenTarget = 0, blueTarget = 0;
+   static short redInitial = 0, greenInitial = 0, blueInitial = 0;
+   static short redPrevious = 0, greenPrevious = 0, bluePrevious = 0;
+   static short redLevel = 0, greenLevel = 0, blueLevel = 0;
+   static bool newColor = false;
+   static unsigned long nextDIPSample = 0;
+   unsigned long nowMillis = millis();
+   static unsigned long nextRampMillis;
+   static unsigned long rampStartMillis;
+   static unsigned long restingEndMillis;
+   static unsigned long reportMillis = 0;
+   static unsigned long repCount = 0;
+   nowMillis = millis();
+   if ( nowMillis >= reportMillis ) {
+      repCount++;
+      //Serial.print("Still alive... ("); Serial.print(repCount); Serial.println(")");
+      /*
+      String debugOutput = "Current State: C=";
+      debugOutput += String(redLevel) + "," + String(greenLevel) + "," + String(blueLevel) + " ";
+      debugOutput += "T=" + String(redTarget) + "," + String(greenTarget) + "," + String(blueTarget) + " ";
+      debugOutput += "Index=" + String(autoColorTargetIndex) + " Ramp=" + String(rampDuration);
+      Serial.println(debugOutput);
+      */
+      reportMillis = nowMillis + 1000;
+   }
+
+   // Check to see if we have a new packet waiting and parse it out if we do
+   int packetSize = listener.parsePacket();
+   if ( packetSize ) {
+      if ( processMessage() ) {
+         // Always reset newColor and resting states when we get a new UDP color command
+         newColor = true;
+         resting = false;
+      }
+   }
+
+   if ( newColor ) {
+      // Reset rampColors Debugging
+      for ( int i=0; i<200; i++ ) { rampValues[i] = ""; };
+      rampValuesIndex = 0;
+      // If the incoming auto-color pattern (from a CMD_AUTOPATTERN) has a single all zero color,
+      // and autoMode is AUTO_ACTIVE then generate colors randomly instead of from the pattern
+      if ( (autoMode == AUTO_ACTIVE) && (numAutoColors == 1) && (autoColors[0].red == 0) && (autoColors[0].green == 0) && (autoColors[0].blue == 0) ) {
+         //redTarget = pgm_read_word(&gamma10[random(0, MAX_ANALOG)]);
+         //greenTarget = pgm_read_word(&gamma10[random(0, MAX_ANALOG)]);
+         //blueTarget = pgm_read_word(&gamma10[random(0, MAX_ANALOG)]);
+         redTarget = gamma10[random(0, MAX_ANALOG)];
+         greenTarget = gamma10[random(0, MAX_ANALOG)];
+         blueTarget = gamma10[random(0, MAX_ANALOG)];
+      } else {
+         //redTarget = pgm_read_word(&gamma10[autoColors[autoColorTargetIndex].red]);
+         //greenTarget = pgm_read_word(&gamma10[autoColors[autoColorTargetIndex].green]);
+         //blueTarget = pgm_read_word(&gamma10[autoColors[autoColorTargetIndex].blue]);
+         redTarget = gamma10[autoColors[autoColorTargetIndex].red];
+         greenTarget = gamma10[autoColors[autoColorTargetIndex].green];
+         blueTarget = gamma10[autoColors[autoColorTargetIndex].blue];
+      }
+      redInitial = redLevel;
+      greenInitial = greenLevel;
+      blueInitial = blueLevel;
+      newColor = false;
+      rampStartMillis = nowMillis;
+      nextRampMillis = rampStartMillis;
+      if ( DEBUG ) {
+         String debugOutput = "Switching to new color: C=";
+         debugOutput += String(redInitial) + "," + String(greenInitial) + "," + String(blueInitial) + " ";
+         debugOutput += "T=" + String(redTarget) + "," + String(greenTarget) + "," + String(blueTarget) + " ";
+         debugOutput += "Index=" + String(autoColorTargetIndex) + " Ramp=" + String(rampDuration);
+         Serial.println(debugOutput);
+      }
+   }
+
+   if ( (redLevel != redTarget) || (greenLevel != greenTarget) || (blueLevel != blueTarget) ) {
+      // Calculate the current rampInterval (how many milliseconds between start of ramp and now)
+      unsigned long rampInterval = nowMillis - rampStartMillis;
+
+      // If nowMillis wrapped around the max INT32 we will just go straight to the last iteration of the ramp
+      if ( rampInterval > rampDuration ) rampInterval = rampDuration;
+
+      // If we aren't at the target values yet, map the current number of milliseconds
+      // into the ramp (rampInterval) to the (color)Initial -> (color)Target values
+      if ( redLevel != redTarget ) {
+         redLevel = map(rampInterval, 0, rampDuration, redInitial, redTarget);
+         if ( abs(redTarget - redLevel) == 1 ) redLevel = redTarget;
+      }
+      if ( greenLevel != greenTarget ) {
+         greenLevel = map(rampInterval, 0, rampDuration, greenInitial, greenTarget);
+         if ( abs(greenTarget - greenLevel) == 1 ) greenLevel = greenTarget;
+      }
+      if ( blueLevel != blueTarget ) {
+         blueLevel = map(rampInterval, 0, rampDuration, blueInitial, blueTarget);
+         if ( abs(blueTarget - blueLevel) == 1 ) blueLevel = blueTarget;
+      }
+      if ( (nowMillis >= nextRampMillis) || ((redLevel == redTarget) && (greenLevel == greenTarget) && (blueLevel == blueTarget)) ) {
+         if ( redLevel != redPrevious ) {
+            analogWrite(GPIO_RED, redLevel);
+            redPrevious = redLevel;
+         }
+         if ( greenLevel != greenPrevious ) {
+            analogWrite(GPIO_GREEN, greenLevel);
+            greenPrevious = greenLevel;
+         }
+         if ( blueLevel != bluePrevious ) {
+            analogWrite(GPIO_BLUE, blueLevel);
+            bluePrevious = blueLevel;
+         }
+         //rampValues[rampValuesIndex] = (String)"(" + (nowMillis - nextRampMillis) + "," + rampValuesIndex + ")" + "(" + redLevel + "," + greenLevel + "," + blueLevel + "),";
+         //rampValuesIndex++;
+         nextRampMillis = nowMillis + 15;
+      }
+   // If all the colors are at their targets and we aren't resting, then start resting
+   } else if ( !resting ) {
+      resting = true;
+      restingEndMillis = nowMillis + autoColors[autoColorTargetIndex].restDuration;
+      if ( DEBUG ) {
+         String debugOutput = "Now resting for: ";
+         debugOutput += String(autoColors[autoColorTargetIndex].restDuration) + " on ";
+         debugOutput += String(autoColorTargetIndex);
+         Serial.println(debugOutput);
+         //Serial.print("rampValues=[");
+         //for ( int i=0; i<rampValuesIndex; i++ ) Serial.print(rampValues[i]);
+         //Serial.println("]");
+      }
+   // If we are resting and the restingEndMillis passes then it is time to ramp to
+   // the next color IF AND ONLY IF we are in a pattern (i.e. don't keep ramping if we're on a static color)
+   } else if ( (nowMillis >= restingEndMillis) && (autoMode == AUTO_ACTIVE) ) {
+      if ( DEBUG ) { Serial.print("Done resting on index=" + String(autoColorTargetIndex)); }
+      resting = false;
+      newColor = true;
+      autoColorTargetIndex = (autoColorTargetIndex < (numAutoColors - 1)) ? autoColorTargetIndex + 1 : 0;
+   }
+
+   // If the DIP switches change, go ahead and adjust myTargetID accordingly. Saves having to restart.
+   // In order to save clock cycles this is only done every 10 seconds.
+   if ( ID_SWITCHED && (nowMillis >= nextDIPSample) ) {
+      byte newVal = 0;
+      bitWrite(newVal, 0, ((digitalRead(ID_BIT_0) == HIGH) ? 1 : 0));
+      bitWrite(newVal, 1, ((digitalRead(ID_BIT_1) == HIGH) ? 1 : 0));
+      bitWrite(newVal, 2, ((digitalRead(ID_BIT_2) == HIGH) ? 1 : 0));
+      bitWrite(newVal, 3, ((digitalRead(ID_BIT_3) == HIGH) ? 1 : 0));
+      if ( myTargetID != newVal ) {
+         myTargetID = newVal;
+         if ( DEBUG ) {
+            Serial.println("BIT_0: " + String(digitalRead(ID_BIT_0)));
+            Serial.println("BIT_1: " + String(digitalRead(ID_BIT_1)));
+            Serial.println("BIT_2: " + String(digitalRead(ID_BIT_2)));
+            Serial.println("BIT_3: " + String(digitalRead(ID_BIT_3)));
+            Serial.println("New Target ID Detected: " + String(myTargetID));
+         }
+      }
+      nextDIPSample = nowMillis + 10000; // Sample the DIP switches every 10 seconds
+   }
+
+   yield();
+}
+
+//const uint16_t PROGMEM gamma10[] {
+const uint16_t gamma10[] {
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,
+1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,
+2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,
+4,4,4,4,4,4,4,4,4,4,4,4,4,5,5,
+5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,
+6,6,6,7,7,7,7,7,7,7,7,7,8,8,8,
+8,8,8,8,8,9,9,9,9,9,9,9,9,10,10,
+10,10,10,10,10,11,11,11,11,11,11,12,12,12,12,
+12,12,12,13,13,13,13,13,13,14,14,14,14,14,15,
+15,15,15,15,15,16,16,16,16,16,17,17,17,17,17,
+18,18,18,18,18,19,19,19,19,20,20,20,20,20,21,
+21,21,21,22,22,22,22,23,23,23,23,24,24,24,24,
+25,25,25,25,26,26,26,26,27,27,27,27,28,28,28,
+29,29,29,29,30,30,30,31,31,31,31,32,32,32,33,
+33,33,34,34,34,35,35,35,35,36,36,36,37,37,37,
+38,38,38,39,39,40,40,40,41,41,41,42,42,42,43,
+43,43,44,44,45,45,45,46,46,46,47,47,48,48,48,
+49,49,50,50,50,51,51,52,52,52,53,53,54,54,55,
+55,55,56,56,57,57,58,58,58,59,59,60,60,61,61,
+62,62,63,63,63,64,64,65,65,66,66,67,67,68,68,
+69,69,70,70,71,71,72,72,73,73,74,74,75,75,76,
+76,77,77,78,79,79,80,80,81,81,82,82,83,83,84,
+85,85,86,86,87,87,88,89,89,90,90,91,92,92,93,
+93,94,95,95,96,96,97,98,98,99,99,100,101,101,102,
+103,103,104,105,105,106,106,107,108,108,109,110,110,111,112,
+112,113,114,115,115,116,117,117,118,119,119,120,121,122,122,
+123,124,124,125,126,127,127,128,129,130,130,131,132,132,133,
+134,135,136,136,137,138,139,139,140,141,142,143,143,144,145,
+146,146,147,148,149,150,151,151,152,153,154,155,155,156,157,
+158,159,160,161,161,162,163,164,165,166,167,167,168,169,170,
+171,172,173,174,175,175,176,177,178,179,180,181,182,183,184,
+185,186,186,187,188,189,190,191,192,193,194,195,196,197,198,
+199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,
+214,215,216,217,218,219,220,221,222,223,224,225,226,228,229,
+230,231,232,233,234,235,236,237,238,239,241,242,243,244,245,
+246,247,248,249,251,252,253,254,255,256,257,259,260,261,262,
+263,264,266,267,268,269,270,272,273,274,275,276,278,279,280,
+281,282,284,285,286,287,289,290,291,292,294,295,296,297,299,
+300,301,302,304,305,306,308,309,310,311,313,314,315,317,318,
+319,321,322,323,325,326,327,329,330,331,333,334,336,337,338,
+340,341,342,344,345,347,348,349,351,352,354,355,356,358,359,
+361,362,364,365,366,368,369,371,372,374,375,377,378,380,381,
+383,384,386,387,389,390,392,393,395,396,398,399,401,402,404,
+405,407,408,410,412,413,415,416,418,419,421,423,424,426,427,
+429,431,432,434,435,437,439,440,442,444,445,447,448,450,452,
+453,455,457,458,460,462,463,465,467,468,470,472,474,475,477,
+479,480,482,484,486,487,489,491,493,494,496,498,500,501,503,
+505,507,509,510,512,514,516,518,519,521,523,525,527,528,530,
+532,534,536,538,539,541,543,545,547,549,551,553,554,556,558,
+560,562,564,566,568,570,572,574,575,577,579,581,583,585,587,
+589,591,593,595,597,599,601,603,605,607,609,611,613,615,617,
+619,621,623,625,627,629,631,633,635,637,640,642,644,646,648,
+650,652,654,656,658,660,663,665,667,669,671,673,675,678,680,
+682,684,686,688,690,693,695,697,699,701,704,706,708,710,712,
+715,717,719,721,724,726,728,730,733,735,737,739,742,744,746,
+749,751,753,755,758,760,762,765,767,769,772,774,776,779,781,
+783,786,788,790,793,795,798,800,802,805,807,810,812,814,817,
+819,822,824,827,829,831,834,836,839,841,844,846,849,851,854,
+856,859,861,864,866,869,871,874,876,879,881,884,887,889,892,
+894,897,899,902,905,907,910,912,915,918,920,923,925,928,931,
+933,936,939,941,944,947,949,952,955,957,960,963,965,968,971,
+973,976,979,982,984,987,990,992,995,998,1001,1004,1006,1009,1012,
+1015,1017,1020,1023 };
diff --git a/test/README b/test/README
new file mode 100644
index 0000000..9b1e87b
--- /dev/null
+++ b/test/README
@@ -0,0 +1,11 @@
+
+This directory is intended for PlatformIO Test Runner and project tests.
+
+Unit Testing is a software testing method by which individual units of
+source code, sets of one or more MCU program modules together with associated
+control data, usage procedures, and operating procedures, are tested to
+determine whether they are fit for use. Unit testing finds problems early
+in the development cycle.
+
+More information about PlatformIO Unit Testing:
+- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html