diff --git a/data/index.htm b/data/index.htm
deleted file mode 100644
index 1a8f1482dd7a819c37485b377186858a431ee729..0000000000000000000000000000000000000000
--- a/data/index.htm
+++ /dev/null
@@ -1,64 +0,0 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
-    <title>Events Log</title>
-    <style type="text/css" media="screen">
-    body {
-      margin:0;
-      padding:0;
-      background-color: black;
-    }
-
-    #dbg {
-      font-family: monaco;
-      font-size: 12px;
-      line-height: 13px;
-      color: #AAA;
-      margin:0;
-      padding:0;
-      padding-left:4px;
-    }
-    </style>
-    <script type="text/javascript">
-    function ge(s){ return document.getElementById(s);}
-    function ce(s){ return document.createElement(s);}
-    function stb(){ window.scrollTo(0, document.body.scrollHeight || document.documentElement.scrollHeight); }
-    function addMessage(m){
-      var msg = ce("div");
-      msg.innerText = m;
-      ge("dbg").appendChild(msg);
-      stb();
-    }
-    function startEvents(){
-      var es = new EventSource('/events');
-      es.onopen = function(e) {
-        addMessage("Events Opened");
-      };
-      es.onerror = function(e) {
-        if (e.target.readyState != EventSource.OPEN) {
-          addMessage("Events Closed");
-        }
-      };
-      es.onmessage = function(e) {
-        addMessage("Event: " + e.data);
-      };
-      es.addEventListener('ota', function(e) {
-        addMessage("[OTA] " + e.data);
-      }, false);
-      es.addEventListener('game', function(e) {
-        addMessage("[GAME] " + e.data);
-      }, false);
-      es.addEventListener('display', function(e) {
-        addMessage("[DISPLAY] " + e.data);
-      }, false);
-    }
-    function onBodyLoad(){
-      startEvents();
-    }
-    </script>
-  </head>
-  <body id="body" onload="onBodyLoad()">
-    <pre id="dbg"></pre>
-  </body>
-</html>
diff --git a/data/index.html b/data/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..f167d3a3847638315026c4c4dce29a8a5f8c1ac9
--- /dev/null
+++ b/data/index.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+    <title>WebSocketTester</title>
+    <style type="text/css" media="screen">
+    body {
+      margin:0;
+      padding:0;
+      background-color: black;
+    }
+
+    #dbg, #input_div, #input_el {
+      font-family: monaco;
+      font-size: 12px;
+      line-height: 13px;
+      color: #AAA;
+    }
+
+    #dbg, #input_div {
+      margin:0;
+      padding:0;
+      padding-left:4px;
+    }
+
+    #input_el {
+      width:98%;
+      background-color: rgba(0,0,0,0);
+      border: 0px;
+    }
+    #input_el:focus {
+      outline: none;
+    }
+    </style>
+    <script type="text/javascript">
+    var ws = null;
+    function ge(s){ return document.getElementById(s);}
+    function ce(s){ return document.createElement(s);}
+    function stb(){ window.scrollTo(0, document.body.scrollHeight || document.documentElement.scrollHeight); }
+    function addMessage(m){
+      var msg = ce("div");
+      msg.innerText = m;
+      ge("dbg").appendChild(msg);
+      stb();
+    }
+    function startSocket(){
+      ws = new WebSocket('ws://'+document.location.host+'/ws',['arduino']);
+      ws.onopen = function(e){
+        addMessage("Connected");
+      };
+      ws.onclose = function(e){
+        addMessage("Disconnected");
+      };
+      ws.onerror = function(e){
+        console.log("ws error", e);
+        addMessage("Error");
+      };
+      ws.onmessage = function(e){
+        var msg = "> "+e.data;
+        addMessage(msg);
+      };
+      ge("input_el").onkeydown = function(e){
+        stb();
+        if(e.keyCode == 13 && ge("input_el").value != ""){
+          ws.send(ge("input_el").value);
+          ge("input_el").value = "";
+        }
+      }
+    }
+    function onBodyLoad(){
+      startSocket();
+    }
+    </script>
+  </head>
+  <body id="body" onload="onBodyLoad()">
+    <pre id="dbg"></pre>
+    <div id="input_div">
+      $<input type="text" value="" id="input_el">
+    </div>
+  </body>
+</html>
diff --git a/platformio.ini b/platformio.ini
index b8f71df7a07e842b90e3e9a222fc647f0e6225d9..e97b462c0d37ba3970e1bb3434f7f4d879538650 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -17,12 +17,14 @@ monitor_port = COM8
 monitor_speed = 115200
 monitor_filters = esp32_exception_decoder
 board_build.filesystem = littlefs
+platform_packages = framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.9
+board_build.arduino.upstream_packages = no
 lib_deps = 
     https://github.com/ayushsharma82/ESPConnect.git
+    https://github.com/bblanchon/ArduinoJson.git
 	https://github.com/me-no-dev/AsyncTCP.git
 	https://github.com/me-no-dev/ESPAsyncWebServer.git
     https://github.com/PlummersSoftwareLLC/SmartMatrix.git
-    https://github.com/sstaub/TickTwo.git
     https://github.com/LennartHennigs/ESPTelnet.git
 
 [env:esp32dev_ota]
diff --git a/src/display.cpp b/src/display.cpp
index 6aaf3009d4b4f4dd2b0765ca2d3587e514f0abea..a523205677b09278cc35280bb890b5aea00eeff5 100644
--- a/src/display.cpp
+++ b/src/display.cpp
@@ -14,7 +14,7 @@ const uint8_t kMatrixOptions = (SMARTMATRIX_OPTIONS_NONE);    // see http://docs
 const uint8_t kBackgroundLayerOptions = (SM_BACKGROUND_OPTIONS_NONE);
 const uint8_t kIndexedLayerOptions = (SM_INDEXED_OPTIONS_NONE);
 
-const int defaultBrightness = (brightnessPercent * 255) / 100;
+int defaultBrightness = (brightnessPercent * 255) / 100;
 
 rgb24 colorWhite = {0xff, 0xff, 0xff};
 
@@ -71,9 +71,16 @@ void showEndScreen(int ticks)
   indexedLayer.swapBuffers(false);
 }
 
+void displayBrightness(int brightness)
+{
+  //logLine("Display Brightness: " + (String)brightness);
+  // brightness = lightPowerMap8bit[brightness];
+  matrix.setBrightness(brightness);
+}
+
 void gameBrightness(int brightness)
 {
-  logLine("Game Brightness: " + (String)brightness);
+  //logLine("Game Brightness: " + (String)brightness);
   // brightness = lightPowerMap8bit[brightness];
   backgroundLayer.setBrightness(brightness);
 }
diff --git a/src/display.h b/src/display.h
index e550e2171e544f69c7213ee01475dd98ad14f3e4..be4764cd0121af833f5f89c617f126236b14e788 100644
--- a/src/display.h
+++ b/src/display.h
@@ -13,6 +13,7 @@ void setupDisplay();
 void displayLoop();
 void showEndScreen(int ticks);
 void showMessage(char* msg);
+void displayBrightness(int brightness);
 void gameBrightness(int brightness);
 void clearDisplay();
 
diff --git a/src/gameoflife.cpp b/src/gameoflife.cpp
index 3c45507e0ed5d1d7fd66430aacd886cbdb773923..421183832c6c487205d1e405c7512af1171de494 100644
--- a/src/gameoflife.cpp
+++ b/src/gameoflife.cpp
@@ -135,9 +135,9 @@ void gameLoop()
 
     if (currentTick % 2)
     {
-      char msg[80];
-      sprintf(msg, "Tick: %4d, Cells now: %4d, before: %4d - No evolution since: %3d", currentTick, cellsAliveNow, cellsAliveBefore, noEvolutionTicks);
-      logLine(msg);
+      // char msg[80];
+      // sprintf(msg, "Tick: %4d, Cells now: %4d, before: %4d - No evolution since: %3d", currentTick, cellsAliveNow, cellsAliveBefore, noEvolutionTicks);
+      // logLine(msg);
       cellsAliveBefore = cellsAliveNow;
     }
 
diff --git a/src/gameoflife.h b/src/gameoflife.h
index ab91666736ff1df1ebaf557ea269cd1d90b9edd7..2e0e14c8412616beb080b0df1d464b641bf36f1c 100644
--- a/src/gameoflife.h
+++ b/src/gameoflife.h
@@ -11,7 +11,7 @@ extern int arrayCopy[SCREEN_HEIGHT][SCREEN_WIDTH];
 
 extern bool runGame;
 extern int noEvolutionTicksLimit;
-extern const int defaultBrightness;
+extern int defaultBrightness;
 
 void setupGameOfLife();
 void createRandomMatrix(int (&a)[SCREEN_HEIGHT][SCREEN_WIDTH]);
diff --git a/src/main.cpp b/src/main.cpp
index 5b7b1aab3bc9f3b06fe981b0e2a3b93fba77daf1..5c4edae84458d2641f5b9a64bc7772f891659eb9 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,5 +1,5 @@
+#define CONFIG_HEAP_CORRUPTION_DETECTION HEAP_POISONING_LIGHT
 #include <Arduino.h>
-#include "TickTwo.h"
 #include "utils.h"
 #include "network.h"
 #include "display.h"
@@ -7,32 +7,34 @@
 
 int noEvolutionTicksLimit = 100;
 int brightnessPercent = 20;
-int gameInterval = 10;
+int gameInterval = 100;
 
-TickTwo gameTimer(gameLoop, gameInterval);
-TickTwo displayTimer(displayLoop, gameInterval);
+unsigned long lastTick;
 
-String titleMsg = "Game of Life";
+void showTitle() {
+  char msg[13];
+  String titleMsg = "Game of Life";
+  titleMsg.toCharArray(msg, 13);
+  showMessage(msg);
+}
 
 void setup()
 {
   Serial.begin(115200);
   logLine("", true);
-  setupNetwork();
   setupDisplay();
-  char msg[13];
-  titleMsg.toCharArray(msg, 13);
-  showMessage(msg);
-  delay(3000);
+  showTitle();
+  setupNetwork();
   clearDisplay();
   setupGameOfLife();
-  gameTimer.start();
-  displayTimer.start();
 }
 
 void loop()
 {
   networkLoop();
-  gameTimer.update();
-  displayTimer.update();
+  if ((millis() - lastTick) >= gameInterval) {
+    gameLoop();
+    displayLoop();
+    lastTick += gameInterval;
+  }
 }
diff --git a/src/network.cpp b/src/network.cpp
index df71d8f3de4c293ac0490e4d4fc10bc3059827e8..c080dba5f38ed6e7cb96d79df201534c9775037c 100644
--- a/src/network.cpp
+++ b/src/network.cpp
@@ -7,11 +7,10 @@
 
 MDNSResponder mdns;
 AsyncWebServer server(80);
+AsyncWebSocket ws("/ws");
 ESPTelnet telnet;
 IPAddress ip;
 
-uint16_t telnetPort = 23;
-
 void setupNetwork()
 {
   setupWifi();
@@ -53,7 +52,13 @@ void setupOTA()
 {
   ArduinoOTA.onStart([]()
                      {
-                      runGame = false; 
+                      runGame = false;
+                      if (ArduinoOTA.getCommand() != U_FLASH) {
+                        LittleFS.end();
+                      }
+                      ws.enable(false);
+                      ws.textAll("OTA Update Started");
+                      ws.closeAll();
                       logLine("Update Start");
                       clearDisplay(); });
   ArduinoOTA.onEnd([]()
@@ -66,9 +71,9 @@ void setupOTA()
     char msg[10];
     sprintf(msg, "OTA:%3d%%", (progress/(total/100)));
     showMessage(msg);
-    char p[32];
-    sprintf(p, "Progress: %u%%\n", (progress/(total/100)));
-    logLine(p); });
+    logLine("OTA: ", false);
+    logLine((String)(progress/(total/100)), false);
+    logLine("%"); });
   ArduinoOTA.onError([](ota_error_t error)
                      {
     if(error == OTA_AUTH_ERROR) logLine("OTA Auth Failed");
@@ -91,7 +96,7 @@ void setupTelnet()
   telnet.onInputReceived(onTelnetInput);
 
   Serial.print("- Telnet: ");
-  if (telnet.begin(telnetPort))
+  if (telnet.begin())
   {
     Serial.println("running");
   }
@@ -104,9 +109,13 @@ void setupTelnet()
 void setupWebserver()
 {
   LittleFS.begin();
-  server.serveStatic("/", LittleFS, "/");
+  ws.onEvent(onEvent);
+  server.addHandler(&ws);
+  server.on("/heap", HTTP_GET, [](AsyncWebServerRequest *request)
+            { request->send(200, "text/plain", String(ESP.getFreeHeap())); });
   server.onNotFound([](AsyncWebServerRequest *request)
                     { request->send(404); });
+  server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html");
   server.begin();
 }
 
@@ -114,9 +123,7 @@ void networkLoop()
 {
   ArduinoOTA.handle();
   if (runGame)
-  {
     telnet.loop();
-  }
 }
 
 void onTelnetConnect(String ip)
@@ -165,3 +172,61 @@ void onTelnetInput(String str)
     telnet.disconnectClient();
   }
 }
+
+DynamicJsonDocument getConfigJson()
+{
+  DynamicJsonDocument doc(1024);
+  doc["brightness"] = defaultBrightness;
+  doc["interval"] = gameInterval;
+  return doc;
+}
+
+void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len)
+{
+  if (type == WS_EVT_CONNECT)
+  {
+    // client connected
+    logLine("WS: connect");
+    client->ping();
+    DynamicJsonDocument doc = getConfigJson();
+    size_t strsize = measureJson(doc) + 1;
+    char json[strsize];
+    serializeJson(doc, json, strsize);
+    logLine("WS>> ", false);
+    logLine(json);
+    ws.text(client->id(), json);
+  }
+  else if (type == WS_EVT_DISCONNECT)
+  {
+    logLine("WS: disconnect");
+  }
+  else if (type == WS_EVT_ERROR)
+  {
+    // error was received from the other end
+    char msg[64];
+    snprintf_P(msg, sizeof(msg), PSTR("WS[%u] error(%u): %s"), client->id(), *((uint16_t *)arg), (char *)data);
+    logLine(msg);
+  }
+  else if (type == WS_EVT_PONG)
+  {
+    // pong message was received (in response to a ping request maybe)
+    logLine("WS: pong");
+  }
+  else if (type == WS_EVT_DATA)
+  {
+    // data packet
+    data[len] = 0;
+    logLine("WS<< ", false);
+    logLine((char *)data);
+    updateConfig(data);
+  }
+}
+
+void updateConfig(uint8_t *data)
+{
+  StaticJsonDocument<200> doc;
+  deserializeJson(doc, (char *)data);
+  defaultBrightness = (int)doc["brightness"];
+  displayBrightness(defaultBrightness);
+  gameInterval = (int)doc["interval"];
+}
diff --git a/src/network.h b/src/network.h
index ae38bd386f77a11a75c8e2b1afcd61755e9a24c0..2177f8228d2c462ad69ef6d8ceb904fc9bf094a3 100644
--- a/src/network.h
+++ b/src/network.h
@@ -10,8 +10,11 @@
 #include <ESPmDNS.h>
 #include <AsyncTCP.h>
 #include <ESPAsyncWebServer.h>
+#include <ArduinoJson.h>
 
 extern bool runGame;
+extern int defaultBrightness;
+extern int gameInterval;
 
 void setupNetwork();
 void setupMDNS();
@@ -25,5 +28,7 @@ void onTelnetDisconnect(String ip);
 void onTelnetReconnect(String ip);
 void onTelnetConnectionAttempt(String ip);
 void onTelnetInput(String str);
+void onEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len);
+void updateConfig (uint8_t* data);
 
 #endif