diff --git a/README.md b/README.md
index 9f69cc338d1395e216aa6192ff6a93bd47ef3b67..665d878304aabc16489aed16cdb7345bc6f2b0ce 100644
--- a/README.md
+++ b/README.md
@@ -73,7 +73,7 @@ To run the program, we launch the main.js file with node, from the command line.
 
 cd to the rndmc folder and run: 
 
-``node main``
+``node rundmc``
 
 It's handy to keep a terminal window open beside a browser when running the software - it's not perfect yet - I do this:
 
@@ -149,11 +149,11 @@ Besides Inputs, Outputs, a Description and State object, anything else goes. I.E
 
 ```javascript
 // boilerplate rndmc header
-const InOut = require('../../lib/jsunit.js')
-let Input = InOut.Input
-let Output = InOut.Output
-let State = InOut.State
-let Button = InOut.Button
+const JSUnit = require('../../src/jsunit.js')
+let Input = JSUnit.Input
+let Output = JSUnit.Output
+let State = JSUnit.State
+let Button = JSUnit.Button
 
 // a constructor, a fn, a javascript mess
 function uiNum() {
@@ -265,27 +265,47 @@ View.assignProgram(program)
 
 # Development Notes 
 
-## For MW
+## Immediately
 
- - want that better-planner
- - do axis pullout separately from stepper motor ?
+- ui / button 
 
- - bug hunting
-  - dereferenced events / stepper axis vector 
-  - multiple button calls ... 
-  - stepper / planner bug 
-  - is load / save really consistent ? what is the state answer ? 
+- would like to send board with new buck out to fab
+- just do stepper23, bc if that's it, that's it ?
 
-- title is still 'xperiment'
-- stepper move.vector is a ref issue? derefed before running calcTrap() ? runs ? in planner ? 
+- tuning:
+ - mrobot having PI, PID terms
+ - having position-set input (also gets output?)
+ - having position-actual output
+ - graphing these things
+ - use UI slider (external) to set PID terms?
+ - should be able to use the same slider element directly inline 
 
-- example modules 
+- cleaning up reps 
+- looking for heirarchy demonstration 
+- imagining microcontrollers 
+- working on robot to inform desires: i.e. ui charts and graphs 
+
+## For Madison Park
+
+- want that better-planner, better-stepper, better-inputs and graphic ui w/ 3js 
+- want network to not blow
+ - tokens w/ crc bit-level ? 
+ - app does keepalive / green-when-on etc ? 
+ - example of setup-for-consistent-feedback of variable, on a timer? tuning loops ... search ... 
+- do axis pullout separately from stepper motor ? accel command ? deep jog desire ... architecture still messy though
+
+- bug hunting
+ - multiline change input paste doesn't work ... big state problem 
+ - is load / save really consistent ? what is the state answer ? 
+ - expected behavior: pressing the button on the raw move module should result in a move for every button press
+ - observed behavior: to send another raw move (via the button on the raw move module), we must reset the motor drivers.
 
-- add reset button to hardware
 - add router for reset, test 
 
-## Documentation
-- GIFS
+## Forever
+
+- open the door, no cuffs 
+- option for 'native' multithreading by opening workers on each event ?
 
 ## Questionable Moves
 - module deletion seems unclean 
@@ -294,8 +314,6 @@ View.assignProgram(program)
 
 ## WRT Representations
 
-OK, should write this out properly at some point.
-
 Module have 
  Inputs
  Outputs
@@ -320,6 +338,11 @@ To assemble a representation of these, we want to have a kind of 'netlist' that,
  - 's' for save program uses hack-asf DOM alert to ask for path
 
 ## Desires 
+- reload / edit individual modules ? 
+- modules spawn new inputs / outputs ? 
+- big UI ? 
+ - arrows catch for jogging 
+- editing ? 
 - heirarchy zoom 
  - architectural clarity betwixt UI and Heap 
 - some auto load / save currently.json file so that we can restart program w/o pain ... maybe just save on new user inputs ? 
@@ -333,6 +356,7 @@ To assemble a representation of these, we want to have a kind of 'netlist' that,
 - consistent dereferencing, type checking implementation?
 
 ## UI Desires
+- scroll / grab events for touchpads etc ... find a mac user and workshop this for one afternoon ? 
 - modules have visual ways to throw errors - i.e. flashing red, popping up... 
 - off-screen divs get pointers-to so that we don't get lost
   - 'h' or something to zoom-to-extents
diff --git a/client/client.js b/client/client.js
index 72aba40ee234066846eec0f13cd8496f45a0c304..4d591fd4b2fc3818686921bdd1267a7f1814fad3 100644
--- a/client/client.js
+++ b/client/client.js
@@ -31,6 +31,8 @@ var lastPos = { x: 10, y: 30 }
 var wrapper = {}
 var nav = {}
 
+var verbose = false 
+
 /*
 
 STARTUP ---------------------------------------------------
@@ -64,7 +66,7 @@ window.onload = function() {
         sckt = this
         // say hello 
         socketSend('console', 'hello server')
-        console.log('socket open')
+        console.log('SCKT: socket open')
         // ask for the current program 
         socketSend('get current program', '')
         // main socket entry point
@@ -75,10 +77,10 @@ window.onload = function() {
         this.onerror = (err) => {
             alert('link to server is broken')
             location.reload()
-            console.log('socket error', err)
+            console.log('SCKT: socket error', err)
         }
         this.onclose = (evt) => {
-            console.log('socket closed', evt)
+            console.log('SCKT: socket closed', evt)
             sckt = null
         }
     }
@@ -110,33 +112,37 @@ function socketRecv(evt) {
             console.log('RECV CONSOLE:', data)
             break
         case 'put module menu':
-            console.log('RECV MODULE MENU')
+            if(verbose) console.log('RECV MODULE MENU')
             heapSendsModuleMenu(data)
             break
         case 'put program menu':
-            console.log('RECV PRG MENU')
+            if(verbose) console.log('RECV PRG MENU')
             heapSendsProgramMenu(data)
             break
         case 'put program':
-            console.log('RECV PROGRAM')
+            if(verbose) console.log('RECV PROGRAM')
             heapSendsNewProgram(data)
             break
         case 'put module':
-            console.log('RECV NEW MODULE')
+            if(verbose) console.log('RECV NEW MODULE')
             heapSendsNewModule(data)
             break
         case 'put module change':
-            console.log('RECV MODULE CHANGE')
+            if(verbose) console.log('RECV MODULE CHANGE')
             heapSendsModuleChange(data)
             break
         case 'put state change':
-            console.log('RECV STATE CHANGE')
+            if(verbose) console.log('RECV STATE CHANGE')
             heapSendsStateChange(data)
             break
+        case 'put ui change':
+            if(verbose) console.log('RECV UI CHANGE') 
+            heapSendsUiChange(data)
+            break 
         case 'restart':
             location.reload()
         default:
-            console.log('ERR recv with non recognized type', recv)
+            console.log('ERR: recv with non recognized type', recv)
             break
     }
 }
@@ -157,7 +163,7 @@ HEAP -> SERVER ---------------------------------------------------
 // always a rep, tho
 var program = {}
 
-// re-writes the program, adds a description,
+// re-writes t program, adds a description,
 // and loads multiple representations of modules to the view 
 
 function heapSendsNewProgram(prgm) {
@@ -166,7 +172,7 @@ function heapSendsNewProgram(prgm) {
     program = prgm
     // 1st we want to git rm old files ... 
     // when adding links, we'll have to add all and then draw links
-    console.log(program)
+    if(verbose) console.log('LOAD PROGRAM', program)
     for (mdlName in program.modules) {
         addRepToView(program.modules[mdlName])
     }
@@ -186,11 +192,11 @@ function heapSendsNewModule(mdl) {
 }
 
 // writes DOM elements to represent the module, appends to the wrapper
-// and appends to the rep object a .ui object 
+// and appends to the rep object a .dom object 
 // containing references to those DOM objects 
 
 function heapSendsModuleChange(data) {
-    console.log(data)
+    if(verbose) console.log('HEAP SENDS MODULE CHANGE', data)
     // data should be rep of changed module 
     var rep = program.modules[data.description.id]
     // we want a general case, but for now we know we're looking for 
@@ -206,7 +212,7 @@ function heapSendsModuleChange(data) {
         var stateItem = rep.state[key]
         if (stateItem != data.state[key]) {
             stateItem = data.state[key]
-            rep.ui.state[key].value = data.state[key]
+            rep.dom.state[key].value = data.state[key]
         }
     }
     // wreckless or wonderful?
@@ -216,16 +222,21 @@ function heapSendsModuleChange(data) {
 
 // update state from server to UI
 function heapSendsStateChange(data) {
-    console.log('HEAP SENDS CHANGE STATE IN MODULE', data)
+    if(verbose) console.log('HEAP SENDS CHANGE STATE IN MODULE', data)
     var rep = program.modules[data.id]
     rep.state[data.key] = data.val
-    if (rep.state[data.key].type == 'multiline') {
-        rep.ui.state[data.key].value = data.val.value
+    if (typeof data.val == 'boolean') {
+        rep.dom.state[data.key].innerHTML = data.key + ':\t\t' + rep.state[data.key].toString()
     } else {
-        rep.ui.state[data.key].value = data.val
+        rep.dom.state[data.key].value = data.val
     }
 }
 
+function heapSendsUiChange(data){
+    if(true) console.log('HEAP SENDS CHANG UI IN MODULE', data) 
+    // 
+}
+
 /*
 
 UI -> HEAP ---------------------------------------------------
@@ -298,14 +309,6 @@ UTILITIES ---------------------------------------------------
 
 */
 
-function isStateKey(key) {
-    if (key.indexOf('_') == 0 || key == 'emitters' || key == 'onChange' || key == 'emitChange') {
-        return false
-    } else {
-        return true
-    }
-}
-
 function redrawLinks() {
     // probably not a great way to do this, we're removing everything
     // svg -rm -r 
@@ -313,14 +316,14 @@ function redrawLinks() {
         svg.removeChild(svg.firstChild)
     }
     // draw origin 
-    var og1 = newLine(-15,0,15,0,5, false)
-    var og2 = newLine(0,-15,0,15,5, false)
+    var og1 = newLine(-15, 0, 15, 0, 5, false)
+    var og2 = newLine(0, -15, 0, 15, 5, false)
     // find that link
     var lnkPt
     var nLnk = 0
     for (mdlName in program.modules) {
         if (program.modules[mdlName].description.isLink) {
-            lnkPt = getLeftWall(program.modules[mdlName].ui.domElem)
+            lnkPt = getLeftWall(program.modules[mdlName].dom.domElem)
         }
     }
     // redraw thru all links, just look at reps
@@ -328,11 +331,11 @@ function redrawLinks() {
         var mdlRep = program.modules[mdlName]
         for (key in mdlRep.outputs) {
             var output = mdlRep.outputs[key]
-            var outputUi = mdlRep.ui.outputs[key]
+            var outputUi = mdlRep.dom.outputs[key]
             for (input in output.calls) {
                 var toId = output.calls[input].parentId
                 var toKey = output.calls[input].key
-                var inputUi = program.modules[toId].ui.inputs[toKey]
+                var inputUi = program.modules[toId].dom.inputs[toKey]
                 var outPos = getOutputArrow(outputUi)
                 var inPos = getInputArrow(inputUi)
                 if (inputUi.isHovering || outputUi.isHovering) {
@@ -344,7 +347,7 @@ function redrawLinks() {
         }
         if (mdlRep.description.isHardware && !mdlRep.description.isLink) {
             nLnk++
-            var hwPt = getRightWall(mdlRep.ui.domElem)
+            var hwPt = getRightWall(mdlRep.dom.domElem)
             lnkPt.y += 5 * nLnk
             var ln = newLine(hwPt.x, hwPt.y, lnkPt.x, lnkPt.y, 7, true)
         }
@@ -463,7 +466,6 @@ function mouseUpDragListener(evt) {
 // get json menu item and render
 // and ask for module at /obj/key
 oncontextmenu = function(evt) {
-    console.log(evt.target)
     if (evt.target.className == 'modname') {
         var modRep = program.modules[evt.target.innerHTML]
         if (modRep) {
@@ -503,6 +505,9 @@ document.onkeydown = function(evt) {
         case 'm':
             socketSend('get module menu', '')
             break
+        case 'd':
+            console.log(program)
+            break
         default:
             break
     }
@@ -511,8 +516,8 @@ document.onkeydown = function(evt) {
 function writeModuleOptionMenu(modRep) {
     var menuDom = document.createElement('div')
     menuDom.id = 'perModuleMenu'
-    menuDom.style.left = 10 + modRep.ui.domElem.offsetLeft + modRep.ui.domElem.offsetWidth + 'px'
-    menuDom.style.top = modRep.ui.domElem.offsetTop + 'px'
+    menuDom.style.left = 10 + modrep.dom.domElem.offsetLeft + modrep.dom.domElem.offsetWidth + 'px'
+    menuDom.style.top = modRep.dom.domElem.offsetTop + 'px'
     // future: rm all inputs, rm all outputs, rename, open (heirarchy)
     var opts = ['delete', 'copy']
     for (i in opts) {
diff --git a/client/divtools.js b/client/divtools.js
index 0a50ee25ecba6cc7bd73928dd4470e52f01cecb7..81c28db3c7adce8aa9aa90edb7cf62be092b8467 100644
--- a/client/divtools.js
+++ b/client/divtools.js
@@ -48,7 +48,7 @@ function addRepToView(rep) {
 
     var uiSetFlag
     // place in pos if info present 
-    // the rep.ui object will store references to the module's related DOM elements
+    // the rep.dom object will store references to the module's related DOM elements
     if (rep.description.position != null) {
         uiSetFlag = false
         if (rep.description.position.left != null) {
@@ -64,50 +64,61 @@ function addRepToView(rep) {
         rep.description.position.top = lastPos.y
     }
 
-    if (rep.ui == null) {
-        rep.ui = {}
+    if (rep.dom == null) {
+        rep.dom = {}
     }
 
-    rep.ui.domElem = domElem
+    rep.dom.domElem = domElem
 
-    // WRITE UI STATE ELEMENTS 
+    // WRITE STATE ELEMENTS 
     var stateElem = document.createElement('div')
     stateElem.className = 'state'
-    rep.ui.state = {}
+    rep.dom.state = {}
     for (st in rep.state) {
         var inputItem = writeStateRep(stateElem, rep, st)
-        rep.ui.state[st] = inputItem
+        rep.dom.state[st] = inputItem
     }
 
     // WRITE INPUTS 
     var inElem = document.createElement('div')
     inElem.className = 'inputs'
-    rep.ui.inputs = {}
+    rep.dom.inputs = {}
     for (ip in rep.inputs) {
         var li = writeEventRep(rep, 'input', ip)
         inElem.appendChild(li)
-        rep.ui.inputs[ip] = li
+        rep.dom.inputs[ip] = li
     }
 
     // WRITE OUTPUTS 
     var outElem = document.createElement('div')
     outElem.className = 'outputs'
-    rep.ui.outputs = {}
+    rep.dom.outputs = {}
     for (op in rep.outputs) {
         var li = writeEventRep(rep, 'output', op)
         outElem.appendChild(li)
-        rep.ui.outputs[op] = li
+        rep.dom.outputs[op] = li
+    }
+
+    // HANDLE UNIQUE UIS
+    if (rep.ui != null) {
+        var uiElem = document.createElement('div')
+        uiElem.className = 'uidiv'
+        rep.dom.ui = {}
+        for (ui in rep.ui) {
+            writeUiElement(uiElem, rep, ui)
+        }
     }
 
     // APPEND TO CONTAINER
     domElem.appendChild(inElem)
     domElem.appendChild(outElem)
     domElem.appendChild(stateElem)
+    domElem.appendChild(uiElem)
     var clearElem = document.createElement('div')
     clearElem.className = 'clear'
     domElem.appendChild(clearElem)
 
-    wrapper.appendChild(rep.ui.domElem)
+    wrapper.appendChild(rep.dom.domElem)
     if (uiSetFlag) {
         putUi(rep)
     }
@@ -124,21 +135,21 @@ function writeEventRep(rep, type, key) {
             name: key,
             evt: evt
         }
-        console.log('clicked', key)
+        if (verbose) console.log('EVENT HOOKUP CLK: ', key)
         evtConnectHandler(ipclk)
     })
     li.addEventListener('mouseover', (evt) => {
         if (type == 'input') {
-            rep.ui.inputs[key].isHovering = true
+            rep.dom.inputs[key].isHovering = true
         } else if (type == 'output') {
-            rep.ui.outputs[key].isHovering = true
+            rep.dom.outputs[key].isHovering = true
         }
         redrawLinks()
         li.addEventListener('mouseout', (evt) => {
             if (type == 'input') {
-                rep.ui.inputs[key].isHovering = false
+                rep.dom.inputs[key].isHovering = false
             } else if (type == 'output') {
-                rep.ui.outputs[key].isHovering = false
+                rep.dom.outputs[key].isHovering = false
             }
             redrawLinks()
         })
@@ -148,70 +159,62 @@ function writeEventRep(rep, type, key) {
 
 function writeStateRep(container, rep, key) {
     var variable = rep.state[key]
-    switch (variable.type) {
-        case 'button':
-            console.log('BUTTON!')
+    switch (typeof variable) {
+        case 'string':
             var li = document.createElement('li')
-            li.appendChild(document.createTextNode(variable.label))
-            li.addEventListener('click', function() {
+            li.appendChild(document.createTextNode(key))
+            var input = document.createElement('input')
+            input.type = 'text'
+            input.size = 24
+            input.value = variable
+            input.addEventListener('change', function() {
+                rep.state[key] = input.value
                 putState(rep, key)
             })
+            li.appendChild(input)
             container.appendChild(li)
-            return li
+            return input
             break
-        case 'multiline':
-            console.log('MULTILINE!')
+        case 'number':
             var li = document.createElement('li')
-            li.appendChild(document.createTextNode(variable.label))
-            li.appendChild(document.createElement('br'))
-            var txtArea = document.createElement('textarea')
-            txtArea.rows = variable.rows
-            txtArea.cols = 25
-            txtArea.value = variable.value
-            txtArea.addEventListener('change', function() {
-                rep.state[key].value = txtArea.value
+            li.appendChild(document.createTextNode(key))
+            var input = document.createElement('input')
+            input.type = 'text'
+            input.size = 24
+            input.value = variable.toString()
+            input.addEventListener('change', function(evt) {
+                rep.state[key] = parseFloat(input.value)
                 putState(rep, key)
             })
-            li.appendChild(txtArea)
+            li.appendChild(input)
             container.appendChild(li)
-            return txtArea
+            return input
             break
-        default:
-            if (typeof variable == 'string') {
-                var li = document.createElement('li')
-                li.appendChild(document.createTextNode(key))
-                var input = document.createElement('input')
-                input.type = 'text'
-                input.size = 24
-                input.value = variable
-                input.addEventListener('change', function() {
-                    rep.state[key] = input.value
-                    putState(rep, key)
-                })
-                li.appendChild(input)
-                container.appendChild(li)
-                return input
-            } else if (typeof variable == 'number') {
+        case 'boolean':
+            var li = document.createElement('li')
+            li.innerHTML = key + ':\t\t' + variable.toString()
+            // TODO: tag align-right? 
+            li.addEventListener('click', function() {
+                if (rep.state[key]) {
+                    rep.state[key] = false
+                } else {
+                    rep.state[key] = true
+                }
+                putState(rep, key)
+            })
+            container.appendChild(li)
+            return li
+            // TODO : return what ? 
+            break
+        case 'object':
+            // first, handle arrays 
+            if (Array.isArray(variable)) {
                 var li = document.createElement('li')
                 li.appendChild(document.createTextNode(key))
                 var input = document.createElement('input')
                 input.type = 'text'
                 input.size = 24
-                input.value = variable.toString()
-                input.addEventListener('change', function(evt) {
-                    rep.state[key] = parseFloat(input.value)
-                    putState(rep, key)
-                })
-                li.appendChild(input)
-                container.appendChild(li)
-                return input
-            } else if (typeof variable == 'object') {
-                if (Array.isArray(variable)) {
-                    var li = document.createElement('li')
-                    li.appendChild(document.createTextNode(key))
-                    var input = document.createElement('input')
-                    input.type = 'text'
-                    input.size = 24
+                if (typeof variable[0] == 'number') {
                     input.value = variable.toString()
                     input.addEventListener('change', function() {
                         var arr = input.value.split(',')
@@ -224,12 +227,86 @@ function writeStateRep(container, rep, key) {
                     li.appendChild(input)
                     container.appendChild(li)
                     return input
+                } else if (typeof variable[0] == 'string') {
+                    input.value = variable.toString()
+                    input.addEventListener('change', function() {
+                        var arr = input.value.split(',')
+                        arr.forEach(function(element, index, array) {
+                            array[index] = element
+                        })
+                        rep.state[key] = arr
+                        putState(rep, key)
+                    })
+                    li.appendChild(input)
+                    container.appendChild(li)
+                    return input
+                } else if (typeof variable[0] == 'object') {
+                    throw 'ERR not going to handle object arrays'
                 }
             } else {
-                console.log("unui'd type:", typeof variable)
+                throw 'ERR not going to handle objects in state'
             }
             break
+        default:
+            console.log("ERR: state walked and no reciprocal code")
+    }
+}
+
+function writeUiElement(container, rep, key) {
+    // pull the representation object from what we were sent 
+    var ui = rep.ui[key]
+    console.log('write ui', ui)
+
+    // load this thing,
+    ui.script = document.createElement('script')
+    ui.script.onerror = function(err){
+        console.log('ERR from ui script', err)
+    }
+    ui.script.onload = function(msg){
+        console.log('script loaded ?')
+    }
+
+    container.appendChild(ui.script)
+
+    ui.script.src = ui.clientPath 
+
+
+
+    // make an xhttp request for the code 
+    /*
+    var request = new XMLHttpRequest()
+    request.open('GET', ui.clientPath)
+    request.responseType = 'text/javascript'
+    request.onLoad = function() {
+        console.log(request.response)
+    }
+    request.send()
+    
+
+    fetch(ui.clientPath).then(function(response) {
+        response.text().then(function(text) {
+            console.log(text)
+            console.log(ui)
+            ui.script.appendChild(document.createTextNode(text))
+            container.apppendChild(ui.script)
+        })
+    })
+    */
+
+    /*
+    // give it access to the socket, 
+    ui.thing.sendToHeap = function(msg) {
+        var data = {
+            id: rep.description.id,
+            key: key,
+            msg: msg
+        }
+        socketSend('put ui change', data)
     }
+
+    container.appendChild(ui.thing.domElement)
+    rep.dom.ui[key] = ui.thing.domElement 
+    */
 }
 
 
@@ -302,11 +379,11 @@ function newLine(x1, y1, x2, y2, stroke, dashed) {
     var ln = {}
     ln.elem = document.createElementNS(svgns, 'line')
     ln.elem.style.stroke = '#1a1a1a'
-    if(dashed){
+    if (dashed) {
         ln.elem.setAttribute('stroke-dasharray', '21, 7, 7, 7')
     }
     ln.elem.style.fill = 'none'
-    if(stroke){
+    if (stroke) {
         ln.elem.style.strokeWidth = stroke + 'px'
     } else {
         ln.elem.style.strokeWidth = '6px'
diff --git a/client/index.html b/client/index.html
index 7ee3bec947104854f142bac4974b661b053b5088..813bbb18924f601da18a8212991e02c52f182fca 100644
--- a/client/index.html
+++ b/client/index.html
@@ -7,8 +7,6 @@
 
 <body>
     <link href="style.css" rel="stylesheet">
-    <!-- <script type="text/javascript" src="dat.gui.js"></script>
-    <script type="text/javascript" src="dummies.js"></script> -->
     <script type="text/javascript" src="divtools.js"></script>
     <script type="text/javascript" src="client.js"></script>
     <div id = "nav">
diff --git a/client/lib/smoothie.js b/client/lib/smoothie.js
new file mode 100644
index 0000000000000000000000000000000000000000..60243a60f3ff7f79f84274376cd95538ccf7d6bb
--- /dev/null
+++ b/client/lib/smoothie.js
@@ -0,0 +1,1100 @@
+// MIT License:
+//
+// Copyright (c) 2010-2013, Joe Walnes
+//               2013-2018, Drew Noakes
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+/**
+ * Smoothie Charts - http://smoothiecharts.org/
+ * (c) 2010-2013, Joe Walnes
+ *     2013-2018, Drew Noakes
+ *
+ * v1.0: Main charting library, by Joe Walnes
+ * v1.1: Auto scaling of axis, by Neil Dunn
+ * v1.2: fps (frames per second) option, by Mathias Petterson
+ * v1.3: Fix for divide by zero, by Paul Nikitochkin
+ * v1.4: Set minimum, top-scale padding, remove timeseries, add optional timer to reset bounds, by Kelley Reynolds
+ * v1.5: Set default frames per second to 50... smoother.
+ *       .start(), .stop() methods for conserving CPU, by Dmitry Vyal
+ *       options.interpolation = 'bezier' or 'line', by Dmitry Vyal
+ *       options.maxValue to fix scale, by Dmitry Vyal
+ * v1.6: minValue/maxValue will always get converted to floats, by Przemek Matylla
+ * v1.7: options.grid.fillStyle may be a transparent color, by Dmitry A. Shashkin
+ *       Smooth rescaling, by Kostas Michalopoulos
+ * v1.8: Set max length to customize number of live points in the dataset with options.maxDataSetLength, by Krishna Narni
+ * v1.9: Display timestamps along the bottom, by Nick and Stev-io
+ *       (https://groups.google.com/forum/?fromgroups#!topic/smoothie-charts/-Ywse8FCpKI%5B1-25%5D)
+ *       Refactored by Krishna Narni, to support timestamp formatting function
+ * v1.10: Switch to requestAnimationFrame, removed the now obsoleted options.fps, by Gergely Imreh
+ * v1.11: options.grid.sharpLines option added, by @drewnoakes
+ *        Addressed warning seen in Firefox when seriesOption.fillStyle undefined, by @drewnoakes
+ * v1.12: Support for horizontalLines added, by @drewnoakes
+ *        Support for yRangeFunction callback added, by @drewnoakes
+ * v1.13: Fixed typo (#32), by @alnikitich
+ * v1.14: Timer cleared when last TimeSeries removed (#23), by @davidgaleano
+ *        Fixed diagonal line on chart at start/end of data stream, by @drewnoakes
+ * v1.15: Support for npm package (#18), by @dominictarr
+ *        Fixed broken removeTimeSeries function (#24) by @davidgaleano
+ *        Minor performance and tidying, by @drewnoakes
+ * v1.16: Bug fix introduced in v1.14 relating to timer creation/clearance (#23), by @drewnoakes
+ *        TimeSeries.append now deals with out-of-order timestamps, and can merge duplicates, by @zacwitte (#12)
+ *        Documentation and some local variable renaming for clarity, by @drewnoakes
+ * v1.17: Allow control over font size (#10), by @drewnoakes
+ *        Timestamp text won't overlap, by @drewnoakes
+ * v1.18: Allow control of max/min label precision, by @drewnoakes
+ *        Added 'borderVisible' chart option, by @drewnoakes
+ *        Allow drawing series with fill but no stroke (line), by @drewnoakes
+ * v1.19: Avoid unnecessary repaints, and fixed flicker in old browsers having multiple charts in document (#40), by @asbai
+ * v1.20: Add SmoothieChart.getTimeSeriesOptions and SmoothieChart.bringToFront functions, by @drewnoakes
+ * v1.21: Add 'step' interpolation mode, by @drewnoakes
+ * v1.22: Add support for different pixel ratios. Also add optional y limit formatters, by @copacetic
+ * v1.23: Fix bug introduced in v1.22 (#44), by @drewnoakes
+ * v1.24: Fix bug introduced in v1.23, re-adding parseFloat to y-axis formatter defaults, by @siggy_sf
+ * v1.25: Fix bug seen when adding a data point to TimeSeries which is older than the current data, by @Nking92
+ *        Draw time labels on top of series, by @comolosabia
+ *        Add TimeSeries.clear function, by @drewnoakes
+ * v1.26: Add support for resizing on high device pixel ratio screens, by @copacetic
+ * v1.27: Fix bug introduced in v1.26 for non whole number devicePixelRatio values, by @zmbush
+ * v1.28: Add 'minValueScale' option, by @megawac
+ *        Fix 'labelPos' for different size of 'minValueString' 'maxValueString', by @henryn
+ * v1.29: Support responsive sizing, by @drewnoakes
+ * v1.29.1: Include types in package, and make property optional, by @TrentHouliston
+ * v1.30: Fix inverted logic in devicePixelRatio support, by @scanlime
+ * v1.31: Support tooltips, by @Sly1024 and @drewnoakes
+ * v1.32: Support frame rate limit, by @dpuyosa
+ * v1.33: Use Date static method instead of instance, by @nnnoel
+ *        Fix bug with tooltips when multiple charts on a page, by @jpmbiz70
+ * v1.34: Add disabled option to TimeSeries, by @TechGuard (#91)
+ *        Add nonRealtimeData option, by @annazhelt (#92, #93)
+ *        Add showIntermediateLabels option, by @annazhelt (#94)
+ *        Add displayDataFromPercentile option, by @annazhelt (#95)
+ *        Fix bug when hiding tooltip element, by @ralphwetzel (#96)
+ *        Support intermediate y-axis labels, by @beikeland (#99)
+ * v1.35: Fix issue with responsive mode at high DPI, by @drewnoakes (#101)
+ * v1.36: Add tooltipLabel to ITimeSeriesPresentationOptions.
+ *        If tooltipLabel is present, tooltipLabel displays inside tooltip
+ *        next to value, by @jackdesert (#102)
+ *        Fix bug rendering issue in series fill when using scroll backwards, by @olssonfredrik
+ *        Add title option, by @mesca
+ */
+
+;(function(exports) {
+
+  // Date.now polyfill
+  Date.now = Date.now || function() { return new Date().getTime(); };
+
+  var Util = {
+    extend: function() {
+      arguments[0] = arguments[0] || {};
+      for (var i = 1; i < arguments.length; i++)
+      {
+        for (var key in arguments[i])
+        {
+          if (arguments[i].hasOwnProperty(key))
+          {
+            if (typeof(arguments[i][key]) === 'object') {
+              if (arguments[i][key] instanceof Array) {
+                arguments[0][key] = arguments[i][key];
+              } else {
+                arguments[0][key] = Util.extend(arguments[0][key], arguments[i][key]);
+              }
+            } else {
+              arguments[0][key] = arguments[i][key];
+            }
+          }
+        }
+      }
+      return arguments[0];
+    },
+    binarySearch: function(data, value) {
+      var low = 0,
+          high = data.length;
+      while (low < high) {
+        var mid = (low + high) >> 1;
+        if (value < data[mid][0])
+          high = mid;
+        else
+          low = mid + 1;
+      }
+      return low;
+    }
+  };
+
+  /**
+   * Initialises a new <code>TimeSeries</code> with optional data options.
+   *
+   * Options are of the form (defaults shown):
+   *
+   * <pre>
+   * {
+   *   resetBounds: true,        // enables/disables automatic scaling of the y-axis
+   *   resetBoundsInterval: 3000 // the period between scaling calculations, in millis
+   * }
+   * </pre>
+   *
+   * Presentation options for TimeSeries are specified as an argument to <code>SmoothieChart.addTimeSeries</code>.
+   *
+   * @constructor
+   */
+  function TimeSeries(options) {
+    this.options = Util.extend({}, TimeSeries.defaultOptions, options);
+    this.disabled = false;
+    this.clear();
+  }
+
+  TimeSeries.defaultOptions = {
+    resetBoundsInterval: 3000,
+    resetBounds: true
+  };
+
+  /**
+   * Clears all data and state from this TimeSeries object.
+   */
+  TimeSeries.prototype.clear = function() {
+    this.data = [];
+    this.maxValue = Number.NaN; // The maximum value ever seen in this TimeSeries.
+    this.minValue = Number.NaN; // The minimum value ever seen in this TimeSeries.
+  };
+
+  /**
+   * Recalculate the min/max values for this <code>TimeSeries</code> object.
+   *
+   * This causes the graph to scale itself in the y-axis.
+   */
+  TimeSeries.prototype.resetBounds = function() {
+    if (this.data.length) {
+      // Walk through all data points, finding the min/max value
+      this.maxValue = this.data[0][1];
+      this.minValue = this.data[0][1];
+      for (var i = 1; i < this.data.length; i++) {
+        var value = this.data[i][1];
+        if (value > this.maxValue) {
+          this.maxValue = value;
+        }
+        if (value < this.minValue) {
+          this.minValue = value;
+        }
+      }
+    } else {
+      // No data exists, so set min/max to NaN
+      this.maxValue = Number.NaN;
+      this.minValue = Number.NaN;
+    }
+  };
+
+  /**
+   * Adds a new data point to the <code>TimeSeries</code>, preserving chronological order.
+   *
+   * @param timestamp the position, in time, of this data point
+   * @param value the value of this data point
+   * @param sumRepeatedTimeStampValues if <code>timestamp</code> has an exact match in the series, this flag controls
+   * whether it is replaced, or the values summed (defaults to false.)
+   */
+  TimeSeries.prototype.append = function(timestamp, value, sumRepeatedTimeStampValues) {
+    // Rewind until we hit an older timestamp
+    var i = this.data.length - 1;
+    while (i >= 0 && this.data[i][0] > timestamp) {
+      i--;
+    }
+
+    if (i === -1) {
+      // This new item is the oldest data
+      this.data.splice(0, 0, [timestamp, value]);
+    } else if (this.data.length > 0 && this.data[i][0] === timestamp) {
+      // Update existing values in the array
+      if (sumRepeatedTimeStampValues) {
+        // Sum this value into the existing 'bucket'
+        this.data[i][1] += value;
+        value = this.data[i][1];
+      } else {
+        // Replace the previous value
+        this.data[i][1] = value;
+      }
+    } else if (i < this.data.length - 1) {
+      // Splice into the correct position to keep timestamps in order
+      this.data.splice(i + 1, 0, [timestamp, value]);
+    } else {
+      // Add to the end of the array
+      this.data.push([timestamp, value]);
+    }
+
+    this.maxValue = isNaN(this.maxValue) ? value : Math.max(this.maxValue, value);
+    this.minValue = isNaN(this.minValue) ? value : Math.min(this.minValue, value);
+  };
+
+  TimeSeries.prototype.dropOldData = function(oldestValidTime, maxDataSetLength) {
+    // We must always keep one expired data point as we need this to draw the
+    // line that comes into the chart from the left, but any points prior to that can be removed.
+    var removeCount = 0;
+    while (this.data.length - removeCount >= maxDataSetLength && this.data[removeCount + 1][0] < oldestValidTime) {
+      removeCount++;
+    }
+    if (removeCount !== 0) {
+      this.data.splice(0, removeCount);
+    }
+  };
+
+  /**
+   * Initialises a new <code>SmoothieChart</code>.
+   *
+   * Options are optional, and should be of the form below. Just specify the values you
+   * need and the rest will be given sensible defaults as shown:
+   *
+   * <pre>
+   * {
+   *   minValue: undefined,                      // specify to clamp the lower y-axis to a given value
+   *   maxValue: undefined,                      // specify to clamp the upper y-axis to a given value
+   *   maxValueScale: 1,                         // allows proportional padding to be added above the chart. for 10% padding, specify 1.1.
+   *   minValueScale: 1,                         // allows proportional padding to be added below the chart. for 10% padding, specify 1.1.
+   *   yRangeFunction: undefined,                // function({min: , max: }) { return {min: , max: }; }
+   *   scaleSmoothing: 0.125,                    // controls the rate at which y-value zoom animation occurs
+   *   millisPerPixel: 20,                       // sets the speed at which the chart pans by
+   *   enableDpiScaling: true,                   // support rendering at different DPI depending on the device
+   *   yMinFormatter: function(min, precision) { // callback function that formats the min y value label
+   *     return parseFloat(min).toFixed(precision);
+   *   },
+   *   yMaxFormatter: function(max, precision) { // callback function that formats the max y value label
+   *     return parseFloat(max).toFixed(precision);
+   *   },
+   *   yIntermediateFormatter: function(intermediate, precision) { // callback function that formats the intermediate y value labels
+   *     return parseFloat(intermediate).toFixed(precision);
+   *   },
+   *   maxDataSetLength: 2,
+   *   interpolation: 'bezier'                   // one of 'bezier', 'linear', or 'step'
+   *   timestampFormatter: null,                 // optional function to format time stamps for bottom of chart
+   *                                             // you may use SmoothieChart.timeFormatter, or your own: function(date) { return ''; }
+   *   scrollBackwards: false,                   // reverse the scroll direction of the chart
+   *   horizontalLines: [],                      // [ { value: 0, color: '#ffffff', lineWidth: 1 } ]
+   *   grid:
+   *   {
+   *     fillStyle: '#000000',                   // the background colour of the chart
+   *     lineWidth: 1,                           // the pixel width of grid lines
+   *     strokeStyle: '#777777',                 // colour of grid lines
+   *     millisPerLine: 1000,                    // distance between vertical grid lines
+   *     sharpLines: false,                      // controls whether grid lines are 1px sharp, or softened
+   *     verticalSections: 2,                    // number of vertical sections marked out by horizontal grid lines
+   *     borderVisible: true                     // whether the grid lines trace the border of the chart or not
+   *   },
+   *   labels
+   *   {
+   *     disabled: false,                        // enables/disables labels showing the min/max values
+   *     fillStyle: '#ffffff',                   // colour for text of labels,
+   *     fontSize: 15,
+   *     fontFamily: 'sans-serif',
+   *     precision: 2,
+   *     showIntermediateLabels: false,          // shows intermediate labels between min and max values along y axis
+   *     intermediateLabelSameAxis: true,
+   *   },
+   *   title
+   *   {
+   *     text: '',                               // the text to display on the left side of the chart
+   *     fillStyle: '#ffffff',                   // colour for text
+   *     fontSize: 15,
+   *     fontFamily: 'sans-serif',
+   *     verticalAlign: 'middle'                 // one of 'top', 'middle', or 'bottom'
+   *   },
+   *   tooltip: false                            // show tooltip when mouse is over the chart
+   *   tooltipLine: {                            // properties for a vertical line at the cursor position
+   *     lineWidth: 1,
+   *     strokeStyle: '#BBBBBB'
+   *   },
+   *   tooltipFormatter: SmoothieChart.tooltipFormatter, // formatter function for tooltip text
+   *   nonRealtimeData: false,                   // use time of latest data as current time
+   *   displayDataFromPercentile: 1,             // display not latest data, but data from the given percentile
+   *                                             // useful when trying to see old data saved by setting a high value for maxDataSetLength
+   *                                             // should be a value between 0 and 1
+   *   responsive: false,                        // whether the chart should adapt to the size of the canvas
+   *   limitFPS: 0                               // maximum frame rate the chart will render at, in FPS (zero means no limit)
+   * }
+   * </pre>
+   *
+   * @constructor
+   */
+  function SmoothieChart(options) {
+    this.options = Util.extend({}, SmoothieChart.defaultChartOptions, options);
+    this.seriesSet = [];
+    this.currentValueRange = 1;
+    this.currentVisMinValue = 0;
+    this.lastRenderTimeMillis = 0;
+    this.lastChartTimestamp = 0;
+
+    this.mousemove = this.mousemove.bind(this);
+    this.mouseout = this.mouseout.bind(this);
+  }
+
+  /** Formats the HTML string content of the tooltip. */
+  SmoothieChart.tooltipFormatter = function (timestamp, data) {
+      var timestampFormatter = this.options.timestampFormatter || SmoothieChart.timeFormatter,
+          lines = [timestampFormatter(new Date(timestamp))],
+          label;
+
+      for (var i = 0; i < data.length; ++i) {
+        label = data[i].series.options.tooltipLabel || ''
+        if (label !== ''){
+            label = label + ' ';
+        }
+        lines.push('<span style="color:' + data[i].series.options.strokeStyle + '">' +
+        label +
+        this.options.yMaxFormatter(data[i].value, this.options.labels.precision) + '</span>');
+      }
+
+      return lines.join('<br>');
+  };
+
+  SmoothieChart.defaultChartOptions = {
+    millisPerPixel: 20,
+    enableDpiScaling: true,
+    yMinFormatter: function(min, precision) {
+      return parseFloat(min).toFixed(precision);
+    },
+    yMaxFormatter: function(max, precision) {
+      return parseFloat(max).toFixed(precision);
+    },
+    yIntermediateFormatter: function(intermediate, precision) {
+      return parseFloat(intermediate).toFixed(precision);
+    },
+    maxValueScale: 1,
+    minValueScale: 1,
+    interpolation: 'bezier',
+    scaleSmoothing: 0.125,
+    maxDataSetLength: 2,
+    scrollBackwards: false,
+    displayDataFromPercentile: 1,
+    grid: {
+      fillStyle: '#000000',
+      strokeStyle: '#777777',
+      lineWidth: 1,
+      sharpLines: false,
+      millisPerLine: 1000,
+      verticalSections: 2,
+      borderVisible: true
+    },
+    labels: {
+      fillStyle: '#ffffff',
+      disabled: false,
+      fontSize: 10,
+      fontFamily: 'monospace',
+      precision: 2,
+      showIntermediateLabels: false,
+      intermediateLabelSameAxis: true,
+    },
+    title: {
+      text: '',
+      fillStyle: '#ffffff',
+      fontSize: 15,
+      fontFamily: 'monospace',
+      verticalAlign: 'middle'
+    },
+    horizontalLines: [],
+    tooltip: false,
+    tooltipLine: {
+      lineWidth: 1,
+      strokeStyle: '#BBBBBB'
+    },
+    tooltipFormatter: SmoothieChart.tooltipFormatter,
+    nonRealtimeData: false,
+    responsive: false,
+    limitFPS: 0
+  };
+
+  // Based on http://inspirit.github.com/jsfeat/js/compatibility.js
+  SmoothieChart.AnimateCompatibility = (function() {
+    var requestAnimationFrame = function(callback, element) {
+          var requestAnimationFrame =
+            window.requestAnimationFrame        ||
+            window.webkitRequestAnimationFrame  ||
+            window.mozRequestAnimationFrame     ||
+            window.oRequestAnimationFrame       ||
+            window.msRequestAnimationFrame      ||
+            function(callback) {
+              return window.setTimeout(function() {
+                callback(Date.now());
+              }, 16);
+            };
+          return requestAnimationFrame.call(window, callback, element);
+        },
+        cancelAnimationFrame = function(id) {
+          var cancelAnimationFrame =
+            window.cancelAnimationFrame ||
+            function(id) {
+              clearTimeout(id);
+            };
+          return cancelAnimationFrame.call(window, id);
+        };
+
+    return {
+      requestAnimationFrame: requestAnimationFrame,
+      cancelAnimationFrame: cancelAnimationFrame
+    };
+  })();
+
+  SmoothieChart.defaultSeriesPresentationOptions = {
+    lineWidth: 1,
+    strokeStyle: '#ffffff'
+  };
+
+  /**
+   * Adds a <code>TimeSeries</code> to this chart, with optional presentation options.
+   *
+   * Presentation options should be of the form (defaults shown):
+   *
+   * <pre>
+   * {
+   *   lineWidth: 1,
+   *   strokeStyle: '#ffffff',
+   *   fillStyle: undefined,
+   *   tooltipLabel: undefined
+   * }
+   * </pre>
+   */
+  SmoothieChart.prototype.addTimeSeries = function(timeSeries, options) {
+    this.seriesSet.push({timeSeries: timeSeries, options: Util.extend({}, SmoothieChart.defaultSeriesPresentationOptions, options)});
+    if (timeSeries.options.resetBounds && timeSeries.options.resetBoundsInterval > 0) {
+      timeSeries.resetBoundsTimerId = setInterval(
+        function() {
+          timeSeries.resetBounds();
+        },
+        timeSeries.options.resetBoundsInterval
+      );
+    }
+  };
+
+  /**
+   * Removes the specified <code>TimeSeries</code> from the chart.
+   */
+  SmoothieChart.prototype.removeTimeSeries = function(timeSeries) {
+    // Find the correct timeseries to remove, and remove it
+    var numSeries = this.seriesSet.length;
+    for (var i = 0; i < numSeries; i++) {
+      if (this.seriesSet[i].timeSeries === timeSeries) {
+        this.seriesSet.splice(i, 1);
+        break;
+      }
+    }
+    // If a timer was operating for that timeseries, remove it
+    if (timeSeries.resetBoundsTimerId) {
+      // Stop resetting the bounds, if we were
+      clearInterval(timeSeries.resetBoundsTimerId);
+    }
+  };
+
+  /**
+   * Gets render options for the specified <code>TimeSeries</code>.
+   *
+   * As you may use a single <code>TimeSeries</code> in multiple charts with different formatting in each usage,
+   * these settings are stored in the chart.
+   */
+  SmoothieChart.prototype.getTimeSeriesOptions = function(timeSeries) {
+    // Find the correct timeseries to remove, and remove it
+    var numSeries = this.seriesSet.length;
+    for (var i = 0; i < numSeries; i++) {
+      if (this.seriesSet[i].timeSeries === timeSeries) {
+        return this.seriesSet[i].options;
+      }
+    }
+  };
+
+  /**
+   * Brings the specified <code>TimeSeries</code> to the top of the chart. It will be rendered last.
+   */
+  SmoothieChart.prototype.bringToFront = function(timeSeries) {
+    // Find the correct timeseries to remove, and remove it
+    var numSeries = this.seriesSet.length;
+    for (var i = 0; i < numSeries; i++) {
+      if (this.seriesSet[i].timeSeries === timeSeries) {
+        var set = this.seriesSet.splice(i, 1);
+        this.seriesSet.push(set[0]);
+        break;
+      }
+    }
+  };
+
+  /**
+   * Instructs the <code>SmoothieChart</code> to start rendering to the provided canvas, with specified delay.
+   *
+   * @param canvas the target canvas element
+   * @param delayMillis an amount of time to wait before a data point is shown. This can prevent the end of the series
+   * from appearing on screen, with new values flashing into view, at the expense of some latency.
+   */
+  SmoothieChart.prototype.streamTo = function(canvas, delayMillis) {
+    this.canvas = canvas;
+    this.delay = delayMillis;
+    this.start();
+  };
+
+  SmoothieChart.prototype.getTooltipEl = function () {
+    // Create the tool tip element lazily
+    if (!this.tooltipEl) {
+      this.tooltipEl = document.createElement('div');
+      this.tooltipEl.className = 'smoothie-chart-tooltip';
+      this.tooltipEl.style.position = 'absolute';
+      this.tooltipEl.style.display = 'none';
+      document.body.appendChild(this.tooltipEl);
+    }
+    return this.tooltipEl;
+  };
+
+  SmoothieChart.prototype.updateTooltip = function () {
+    var el = this.getTooltipEl();
+
+    if (!this.mouseover || !this.options.tooltip) {
+      el.style.display = 'none';
+      return;
+    }
+
+    var time = this.lastChartTimestamp;
+
+    // x pixel to time
+    var t = this.options.scrollBackwards
+      ? time - this.mouseX * this.options.millisPerPixel
+      : time - (this.canvas.offsetWidth - this.mouseX) * this.options.millisPerPixel;
+
+    var data = [];
+
+     // For each data set...
+    for (var d = 0; d < this.seriesSet.length; d++) {
+      var timeSeries = this.seriesSet[d].timeSeries;
+      if (timeSeries.disabled) {
+          continue;
+      }
+
+      // find datapoint closest to time 't'
+      var closeIdx = Util.binarySearch(timeSeries.data, t);
+      if (closeIdx > 0 && closeIdx < timeSeries.data.length) {
+        data.push({ series: this.seriesSet[d], index: closeIdx, value: timeSeries.data[closeIdx][1] });
+      }
+    }
+
+    if (data.length) {
+      el.innerHTML = this.options.tooltipFormatter.call(this, t, data);
+      el.style.display = 'block';
+    } else {
+      el.style.display = 'none';
+    }
+  };
+
+  SmoothieChart.prototype.mousemove = function (evt) {
+    this.mouseover = true;
+    this.mouseX = evt.offsetX;
+    this.mouseY = evt.offsetY;
+    this.mousePageX = evt.pageX;
+    this.mousePageY = evt.pageY;
+
+    var el = this.getTooltipEl();
+    el.style.top = Math.round(this.mousePageY) + 'px';
+    el.style.left = Math.round(this.mousePageX) + 'px';
+    this.updateTooltip();
+  };
+
+  SmoothieChart.prototype.mouseout = function () {
+    this.mouseover = false;
+    this.mouseX = this.mouseY = -1;
+    if (this.tooltipEl)
+      this.tooltipEl.style.display = 'none';
+  };
+
+  /**
+   * Make sure the canvas has the optimal resolution for the device's pixel ratio.
+   */
+  SmoothieChart.prototype.resize = function () {
+    var dpr = !this.options.enableDpiScaling || !window ? 1 : window.devicePixelRatio,
+        width, height;
+    if (this.options.responsive) {
+      // Newer behaviour: Use the canvas's size in the layout, and set the internal
+      // resolution according to that size and the device pixel ratio (eg: high DPI)
+      width = this.canvas.offsetWidth;
+      height = this.canvas.offsetHeight;
+
+      if (width !== this.lastWidth) {
+        this.lastWidth = width;
+        this.canvas.setAttribute('width', (Math.floor(width * dpr)).toString());
+        this.canvas.getContext('2d').scale(dpr, dpr);
+      }
+      if (height !== this.lastHeight) {
+        this.lastHeight = height;
+        this.canvas.setAttribute('height', (Math.floor(height * dpr)).toString());
+        this.canvas.getContext('2d').scale(dpr, dpr);
+      }
+    } else if (dpr !== 1) {
+      // Older behaviour: use the canvas's inner dimensions and scale the element's size
+      // according to that size and the device pixel ratio (eg: high DPI)
+      width = parseInt(this.canvas.getAttribute('width'));
+      height = parseInt(this.canvas.getAttribute('height'));
+
+      if (!this.originalWidth || (Math.floor(this.originalWidth * dpr) !== width)) {
+        this.originalWidth = width;
+        this.canvas.setAttribute('width', (Math.floor(width * dpr)).toString());
+        this.canvas.style.width = width + 'px';
+        this.canvas.getContext('2d').scale(dpr, dpr);
+      }
+
+      if (!this.originalHeight || (Math.floor(this.originalHeight * dpr) !== height)) {
+        this.originalHeight = height;
+        this.canvas.setAttribute('height', (Math.floor(height * dpr)).toString());
+        this.canvas.style.height = height + 'px';
+        this.canvas.getContext('2d').scale(dpr, dpr);
+      }
+    }
+  };
+
+  /**
+   * Starts the animation of this chart.
+   */
+  SmoothieChart.prototype.start = function() {
+    if (this.frame) {
+      // We're already running, so just return
+      return;
+    }
+
+    this.canvas.addEventListener('mousemove', this.mousemove);
+    this.canvas.addEventListener('mouseout', this.mouseout);
+
+    // Renders a frame, and queues the next frame for later rendering
+    var animate = function() {
+      this.frame = SmoothieChart.AnimateCompatibility.requestAnimationFrame(function() {
+        if(this.options.nonRealtimeData){
+           var dateZero = new Date(0);
+           // find the data point with the latest timestamp
+           var maxTimeStamp = this.seriesSet.reduce(function(max, series){
+             var dataSet = series.timeSeries.data;
+             var indexToCheck = Math.round(this.options.displayDataFromPercentile * dataSet.length) - 1;
+             indexToCheck = indexToCheck >= 0 ? indexToCheck : 0;
+             indexToCheck = indexToCheck <= dataSet.length -1 ? indexToCheck : dataSet.length -1;
+             if(dataSet && dataSet.length > 0)
+             {
+              // timestamp corresponds to element 0 of the data point
+              var lastDataTimeStamp = dataSet[indexToCheck][0];
+              max = max > lastDataTimeStamp ? max : lastDataTimeStamp;
+             }
+             return max;
+          }.bind(this), dateZero);
+          // use the max timestamp as current time
+          this.render(this.canvas, maxTimeStamp > dateZero ? maxTimeStamp : null);
+        } else {
+          this.render();
+        }
+        animate();
+      }.bind(this));
+    }.bind(this);
+
+    animate();
+  };
+
+  /**
+   * Stops the animation of this chart.
+   */
+  SmoothieChart.prototype.stop = function() {
+    if (this.frame) {
+      SmoothieChart.AnimateCompatibility.cancelAnimationFrame(this.frame);
+      delete this.frame;
+      this.canvas.removeEventListener('mousemove', this.mousemove);
+      this.canvas.removeEventListener('mouseout', this.mouseout);
+    }
+  };
+
+  SmoothieChart.prototype.updateValueRange = function() {
+    // Calculate the current scale of the chart, from all time series.
+    var chartOptions = this.options,
+        chartMaxValue = Number.NaN,
+        chartMinValue = Number.NaN;
+
+    for (var d = 0; d < this.seriesSet.length; d++) {
+      // TODO(ndunn): We could calculate / track these values as they stream in.
+      var timeSeries = this.seriesSet[d].timeSeries;
+      if (timeSeries.disabled) {
+          continue;
+      }
+
+      if (!isNaN(timeSeries.maxValue)) {
+        chartMaxValue = !isNaN(chartMaxValue) ? Math.max(chartMaxValue, timeSeries.maxValue) : timeSeries.maxValue;
+      }
+
+      if (!isNaN(timeSeries.minValue)) {
+        chartMinValue = !isNaN(chartMinValue) ? Math.min(chartMinValue, timeSeries.minValue) : timeSeries.minValue;
+      }
+    }
+
+    // Scale the chartMaxValue to add padding at the top if required
+    if (chartOptions.maxValue != null) {
+      chartMaxValue = chartOptions.maxValue;
+    } else {
+      chartMaxValue *= chartOptions.maxValueScale;
+    }
+
+    // Set the minimum if we've specified one
+    if (chartOptions.minValue != null) {
+      chartMinValue = chartOptions.minValue;
+    } else {
+      chartMinValue -= Math.abs(chartMinValue * chartOptions.minValueScale - chartMinValue);
+    }
+
+    // If a custom range function is set, call it
+    if (this.options.yRangeFunction) {
+      var range = this.options.yRangeFunction({min: chartMinValue, max: chartMaxValue});
+      chartMinValue = range.min;
+      chartMaxValue = range.max;
+    }
+
+    if (!isNaN(chartMaxValue) && !isNaN(chartMinValue)) {
+      var targetValueRange = chartMaxValue - chartMinValue;
+      var valueRangeDiff = (targetValueRange - this.currentValueRange);
+      var minValueDiff = (chartMinValue - this.currentVisMinValue);
+      this.isAnimatingScale = Math.abs(valueRangeDiff) > 0.1 || Math.abs(minValueDiff) > 0.1;
+      this.currentValueRange += chartOptions.scaleSmoothing * valueRangeDiff;
+      this.currentVisMinValue += chartOptions.scaleSmoothing * minValueDiff;
+    }
+
+    this.valueRange = { min: chartMinValue, max: chartMaxValue };
+  };
+
+  SmoothieChart.prototype.render = function(canvas, time) {
+    var nowMillis = Date.now();
+
+    // Respect any frame rate limit.
+    if (this.options.limitFPS > 0 && nowMillis - this.lastRenderTimeMillis < (1000/this.options.limitFPS))
+      return;
+
+    if (!this.isAnimatingScale) {
+      // We're not animating. We can use the last render time and the scroll speed to work out whether
+      // we actually need to paint anything yet. If not, we can return immediately.
+
+      // Render at least every 1/6th of a second. The canvas may be resized, which there is
+      // no reliable way to detect.
+      var maxIdleMillis = Math.min(1000/6, this.options.millisPerPixel);
+
+      if (nowMillis - this.lastRenderTimeMillis < maxIdleMillis) {
+        return;
+      }
+    }
+
+    this.resize();
+    this.updateTooltip();
+
+    this.lastRenderTimeMillis = nowMillis;
+
+    canvas = canvas || this.canvas;
+    time = time || nowMillis - (this.delay || 0);
+
+    // Round time down to pixel granularity, so motion appears smoother.
+    time -= time % this.options.millisPerPixel;
+
+    this.lastChartTimestamp = time;
+
+    var context = canvas.getContext('2d'),
+        chartOptions = this.options,
+        dimensions = { top: 0, left: 0, width: canvas.clientWidth, height: canvas.clientHeight },
+        // Calculate the threshold time for the oldest data points.
+        oldestValidTime = time - (dimensions.width * chartOptions.millisPerPixel),
+        valueToYPixel = function(value) {
+          var offset = value - this.currentVisMinValue;
+          return this.currentValueRange === 0
+            ? dimensions.height
+            : dimensions.height - (Math.round((offset / this.currentValueRange) * dimensions.height));
+        }.bind(this),
+        timeToXPixel = function(t) {
+          if(chartOptions.scrollBackwards) {
+            return Math.round((time - t) / chartOptions.millisPerPixel);
+          }
+          return Math.round(dimensions.width - ((time - t) / chartOptions.millisPerPixel));
+        };
+
+    this.updateValueRange();
+
+    context.font = chartOptions.labels.fontSize + 'px ' + chartOptions.labels.fontFamily;
+
+    // Save the state of the canvas context, any transformations applied in this method
+    // will get removed from the stack at the end of this method when .restore() is called.
+    context.save();
+
+    // Move the origin.
+    context.translate(dimensions.left, dimensions.top);
+
+    // Create a clipped rectangle - anything we draw will be constrained to this rectangle.
+    // This prevents the occasional pixels from curves near the edges overrunning and creating
+    // screen cheese (that phrase should need no explanation).
+    context.beginPath();
+    context.rect(0, 0, dimensions.width, dimensions.height);
+    context.clip();
+
+    // Clear the working area.
+    context.save();
+    context.fillStyle = chartOptions.grid.fillStyle;
+    context.clearRect(0, 0, dimensions.width, dimensions.height);
+    context.fillRect(0, 0, dimensions.width, dimensions.height);
+    context.restore();
+
+    // Grid lines...
+    context.save();
+    context.lineWidth = chartOptions.grid.lineWidth;
+    context.strokeStyle = chartOptions.grid.strokeStyle;
+    // Vertical (time) dividers.
+    if (chartOptions.grid.millisPerLine > 0) {
+      context.beginPath();
+      for (var t = time - (time % chartOptions.grid.millisPerLine);
+           t >= oldestValidTime;
+           t -= chartOptions.grid.millisPerLine) {
+        var gx = timeToXPixel(t);
+        if (chartOptions.grid.sharpLines) {
+          gx -= 0.5;
+        }
+        context.moveTo(gx, 0);
+        context.lineTo(gx, dimensions.height);
+      }
+      context.stroke();
+      context.closePath();
+    }
+
+    // Horizontal (value) dividers.
+    for (var v = 1; v < chartOptions.grid.verticalSections; v++) {
+      var gy = Math.round(v * dimensions.height / chartOptions.grid.verticalSections);
+      if (chartOptions.grid.sharpLines) {
+        gy -= 0.5;
+      }
+      context.beginPath();
+      context.moveTo(0, gy);
+      context.lineTo(dimensions.width, gy);
+      context.stroke();
+      context.closePath();
+    }
+    // Bounding rectangle.
+    if (chartOptions.grid.borderVisible) {
+      context.beginPath();
+      context.strokeRect(0, 0, dimensions.width, dimensions.height);
+      context.closePath();
+    }
+    context.restore();
+
+    // Draw any horizontal lines...
+    if (chartOptions.horizontalLines && chartOptions.horizontalLines.length) {
+      for (var hl = 0; hl < chartOptions.horizontalLines.length; hl++) {
+        var line = chartOptions.horizontalLines[hl],
+            hly = Math.round(valueToYPixel(line.value)) - 0.5;
+        context.strokeStyle = line.color || '#ffffff';
+        context.lineWidth = line.lineWidth || 1;
+        context.beginPath();
+        context.moveTo(0, hly);
+        context.lineTo(dimensions.width, hly);
+        context.stroke();
+        context.closePath();
+      }
+    }
+
+    // For each data set...
+    for (var d = 0; d < this.seriesSet.length; d++) {
+      context.save();
+      var timeSeries = this.seriesSet[d].timeSeries;
+      if (timeSeries.disabled) {
+          continue;
+      }
+
+      var dataSet = timeSeries.data,
+          seriesOptions = this.seriesSet[d].options;
+
+      // Delete old data that's moved off the left of the chart.
+      timeSeries.dropOldData(oldestValidTime, chartOptions.maxDataSetLength);
+
+      // Set style for this dataSet.
+      context.lineWidth = seriesOptions.lineWidth;
+      context.strokeStyle = seriesOptions.strokeStyle;
+      // Draw the line...
+      context.beginPath();
+      // Retain lastX, lastY for calculating the control points of bezier curves.
+      var firstX = 0, firstY = 0, lastX = 0, lastY = 0;
+      for (var i = 0; i < dataSet.length && dataSet.length !== 1; i++) {
+        var x = timeToXPixel(dataSet[i][0]),
+            y = valueToYPixel(dataSet[i][1]);
+
+        if (i === 0) {
+          firstX = x;
+          firstY = y;
+          context.moveTo(x, y);
+        } else {
+          switch (chartOptions.interpolation) {
+            case "linear":
+            case "line": {
+              context.lineTo(x,y);
+              break;
+            }
+            case "bezier":
+            default: {
+              // Great explanation of Bezier curves: http://en.wikipedia.org/wiki/Bezier_curve#Quadratic_curves
+              //
+              // Assuming A was the last point in the line plotted and B is the new point,
+              // we draw a curve with control points P and Q as below.
+              //
+              // A---P
+              //     |
+              //     |
+              //     |
+              //     Q---B
+              //
+              // Importantly, A and P are at the same y coordinate, as are B and Q. This is
+              // so adjacent curves appear to flow as one.
+              //
+              context.bezierCurveTo( // startPoint (A) is implicit from last iteration of loop
+                Math.round((lastX + x) / 2), lastY, // controlPoint1 (P)
+                Math.round((lastX + x)) / 2, y, // controlPoint2 (Q)
+                x, y); // endPoint (B)
+              break;
+            }
+            case "step": {
+              context.lineTo(x,lastY);
+              context.lineTo(x,y);
+              break;
+            }
+          }
+        }
+
+        lastX = x; lastY = y;
+      }
+
+      if (dataSet.length > 1) {
+        if (seriesOptions.fillStyle) {
+          // Close up the fill region.
+          if (chartOptions.scrollBackwards) {
+            context.lineTo(lastX, dimensions.height + seriesOptions.lineWidth);
+            context.lineTo(firstX, dimensions.height + seriesOptions.lineWidth);
+            context.lineTo(firstX, firstY);
+          } else {
+            context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, lastY);
+            context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, dimensions.height + seriesOptions.lineWidth + 1);
+            context.lineTo(firstX, dimensions.height + seriesOptions.lineWidth);
+          }
+          context.fillStyle = seriesOptions.fillStyle;
+          context.fill();
+        }
+
+        if (seriesOptions.strokeStyle && seriesOptions.strokeStyle !== 'none') {
+          context.stroke();
+        }
+        context.closePath();
+      }
+      context.restore();
+    }
+
+    if (chartOptions.tooltip && this.mouseX >= 0) {
+      // Draw vertical bar to show tooltip position
+      context.lineWidth = chartOptions.tooltipLine.lineWidth;
+      context.strokeStyle = chartOptions.tooltipLine.strokeStyle;
+      context.beginPath();
+      context.moveTo(this.mouseX, 0);
+      context.lineTo(this.mouseX, dimensions.height);
+      context.closePath();
+      context.stroke();
+      this.updateTooltip();
+    }
+
+    // Draw the axis values on the chart.
+    if (!chartOptions.labels.disabled && !isNaN(this.valueRange.min) && !isNaN(this.valueRange.max)) {
+      var maxValueString = chartOptions.yMaxFormatter(this.valueRange.max, chartOptions.labels.precision),
+          minValueString = chartOptions.yMinFormatter(this.valueRange.min, chartOptions.labels.precision),
+          maxLabelPos = chartOptions.scrollBackwards ? 0 : dimensions.width - context.measureText(maxValueString).width - 2,
+          minLabelPos = chartOptions.scrollBackwards ? 0 : dimensions.width - context.measureText(minValueString).width - 2;
+      context.fillStyle = chartOptions.labels.fillStyle;
+      context.fillText(maxValueString, maxLabelPos, chartOptions.labels.fontSize);
+      context.fillText(minValueString, minLabelPos, dimensions.height - 2);
+    }
+
+    // Display intermediate y axis labels along y-axis to the left of the chart
+    if ( chartOptions.labels.showIntermediateLabels
+          && !isNaN(this.valueRange.min) && !isNaN(this.valueRange.max)
+          && chartOptions.grid.verticalSections > 0) {
+      // show a label above every vertical section divider
+      var step = (this.valueRange.max - this.valueRange.min) / chartOptions.grid.verticalSections;
+      var stepPixels = dimensions.height / chartOptions.grid.verticalSections;
+      for (var v = 1; v < chartOptions.grid.verticalSections; v++) {
+        var gy = dimensions.height - Math.round(v * stepPixels);
+        if (chartOptions.grid.sharpLines) {
+          gy -= 0.5;
+        }
+        var yValue = chartOptions.yIntermediateFormatter(this.valueRange.min + (v * step), chartOptions.labels.precision);
+        //left of right axis?
+        intermediateLabelPos =
+          chartOptions.labels.intermediateLabelSameAxis
+          ? (chartOptions.scrollBackwards ? 0 : dimensions.width - context.measureText(yValue).width - 2)
+          : (chartOptions.scrollBackwards ? dimensions.width - context.measureText(yValue).width - 2 : 0);
+
+        context.fillText(yValue, intermediateLabelPos, gy - chartOptions.grid.lineWidth);
+      }
+    }
+
+    // Display timestamps along x-axis at the bottom of the chart.
+    if (chartOptions.timestampFormatter && chartOptions.grid.millisPerLine > 0) {
+      var textUntilX = chartOptions.scrollBackwards
+        ? context.measureText(minValueString).width
+        : dimensions.width - context.measureText(minValueString).width + 4;
+      for (var t = time - (time % chartOptions.grid.millisPerLine);
+           t >= oldestValidTime;
+           t -= chartOptions.grid.millisPerLine) {
+        var gx = timeToXPixel(t);
+        // Only draw the timestamp if it won't overlap with the previously drawn one.
+        if ((!chartOptions.scrollBackwards && gx < textUntilX) || (chartOptions.scrollBackwards && gx > textUntilX))  {
+          // Formats the timestamp based on user specified formatting function
+          // SmoothieChart.timeFormatter function above is one such formatting option
+          var tx = new Date(t),
+            ts = chartOptions.timestampFormatter(tx),
+            tsWidth = context.measureText(ts).width;
+
+          textUntilX = chartOptions.scrollBackwards
+            ? gx + tsWidth + 2
+            : gx - tsWidth - 2;
+
+          context.fillStyle = chartOptions.labels.fillStyle;
+          if(chartOptions.scrollBackwards) {
+            context.fillText(ts, gx, dimensions.height - 2);
+          } else {
+            context.fillText(ts, gx - tsWidth, dimensions.height - 2);
+          }
+        }
+      }
+    }
+
+    // Display title.
+    if (chartOptions.title.text !== '') {
+      context.font = chartOptions.title.fontSize + 'px ' + chartOptions.title.fontFamily;
+      var titleXPos = chartOptions.scrollBackwards ? dimensions.width - context.measureText(chartOptions.title.text).width - 2 : 2;
+      if (chartOptions.title.verticalAlign == 'bottom') {
+        context.textBaseline = 'bottom';
+        var titleYPos = dimensions.height;
+      } else if (chartOptions.title.verticalAlign == 'middle') {
+        context.textBaseline = 'middle';
+        var titleYPos = dimensions.height / 2;
+      } else {
+        context.textBaseline = 'top';
+        var titleYPos = 0;
+      }
+      context.fillStyle = chartOptions.title.fillStyle;
+      context.fillText(chartOptions.title.text, titleXPos, titleYPos);
+    }
+
+    context.restore(); // See .save() above.
+  };
+
+  // Sample timestamp formatting function
+  SmoothieChart.timeFormatter = function(date) {
+    function pad2(number) { return (number < 10 ? '0' : '') + number }
+    return pad2(date.getHours()) + ':' + pad2(date.getMinutes()) + ':' + pad2(date.getSeconds());
+  };
+
+  exports.TimeSeries = TimeSeries;
+  exports.SmoothieChart = SmoothieChart;
+
+})(typeof exports === 'undefined' ? this : exports);
+
diff --git a/client/ui/uiButton.js b/client/ui/uiButton.js
new file mode 100644
index 0000000000000000000000000000000000000000..dada8bdafad5c846919af0d97981907a9709263e
--- /dev/null
+++ b/client/ui/uiButton.js
@@ -0,0 +1 @@
+console.log('hello lib load')
\ No newline at end of file
diff --git a/files/dogbone.gcode b/files/dogbone.gcode
index 8eada8236f36aefc55308843150274ba79dfa562..fa1b9172c05fb0022b4349ce82f2e52ea03fd92e 100644
--- a/files/dogbone.gcode
+++ b/files/dogbone.gcode
@@ -1,6 +1,6 @@
 G0 F25 Z5
 G0 X27.5Y111.5
-G0 Z-2
+G0 Z3
 G0 Y83.5
 G0 X14.5
 G0 Y27.5
@@ -13,7 +13,7 @@ G0 Y83.5
 G0 X-1.5
 G0 Y111.5
 G0 X27.5
-G0 Z-4
+G0 Z2
 G0 Y83.5
 G0 X14.5
 G0 Y27.5
@@ -26,7 +26,7 @@ G0 Y83.5
 G0 X-1.5
 G0 Y111.5
 G0 X27.5
-G0 Z-6
+G0 Z1
 G0 Y83.5
 G0 X14.5
 G0 Y27.5
@@ -39,5 +39,5 @@ G0 Y83.5
 G0 X-1.5
 G0 Y111.5
 G0 X27.5
-G0 Z10
-G0 X-40Y140
\ No newline at end of file
+G0 Z5
+G0 X10Y100
\ No newline at end of file
diff --git a/modules/flowcontrol/and.js b/modules/flowcontrol/and.js
index e3825b19187349b19dbc2348e101b5be3590f308..d299e60aab6b8cb57dd1d593c7b15dfea72ef8b4 100644
--- a/modules/flowcontrol/and.js
+++ b/modules/flowcontrol/and.js
@@ -1,9 +1,9 @@
 // boilerplate atkapi header
-const InOut = require('../../lib/jsunit.js')
-let Input = InOut.Input
-let Output = InOut.Output
-let State = InOut.State
-let Button = InOut.Button
+const JSUnit = require('../../src/jsunit.js')
+let Input = JSUnit.Input
+let Output = JSUnit.Output
+let State = JSUnit.State
+let Button = JSUnit.Button
 
 // the 'andflow' flowcontrol unit only lets events through once both have occurred ... 
 
diff --git a/modules/hardware/atkbreadboard.js b/modules/hardware/atkbreadboardboard.js
similarity index 89%
rename from modules/hardware/atkbreadboard.js
rename to modules/hardware/atkbreadboardboard.js
index e240dd84ebe86902d3b3401b386b035daa5953e4..98bfd272ff571f9e4d4823d4e5c9e566d31ff29a 100644
--- a/modules/hardware/atkbreadboard.js
+++ b/modules/hardware/atkbreadboardboard.js
@@ -1,12 +1,12 @@
 // boilerplate atkapi header
-const InOut = require('../../lib/jsunit.js')
-let Input = InOut.Input
-let Output = InOut.Output
-let State = InOut.State
-let Button = InOut.Button
-
-const Hardware = require('../../lib/atkunit.js')
-const PCKT = require('../../lib/packets.js')
+const JSUnit = require('../../src/jsunit.js')
+let Input = JSUnit.Input
+let Output = JSUnit.Output
+let State = JSUnit.State
+let Button = JSUnit.Button
+
+const Hardware = require('../../src/atkunit.js')
+const PCKT = require('../../src/packets.js')
 
 // a constructor, a fn, a javascript mess
 function ATKBreadBoardBoard() {
diff --git a/modules/hardware/atkmrobot.js b/modules/hardware/atkmrobot.js
index de127aed0222dfff2dab61c91444201846b65733..70cf5d71a957d54bb5184502cea5ed75cafb64f8 100644
--- a/modules/hardware/atkmrobot.js
+++ b/modules/hardware/atkmrobot.js
@@ -1,12 +1,12 @@
 // boilerplate atkapi header
-const InOut = require('../../lib/jsunit.js')
-let Input = InOut.Input
-let Output = InOut.Output
-let State = InOut.State
-let Button = InOut.Button
-
-const Hardware = require('../../lib/atkunit.js')
-const PCKT = require('../../lib/packets.js')
+const JSUnit = require('../../src/jsunit.js')
+let Input = JSUnit.Input
+let Output = JSUnit.Output
+let State = JSUnit.State
+let Button = JSUnit.Button
+
+const Hardware = require('../../src/atkunit.js')
+const PCKT = require('../../src/packets.js')
 
 // a constructor, a fn, a javascript mess
 function ATKMathRobot() {
diff --git a/modules/hardware/atkseriallink.js b/modules/hardware/atkseriallink.js
index f97d49311b297e7d0f9393aab0d5527b18bb3b55..2c2e6729382f219d88c17f4317d3d9fede22159f 100644
--- a/modules/hardware/atkseriallink.js
+++ b/modules/hardware/atkseriallink.js
@@ -1,10 +1,10 @@
 // boilerplate atkapi header
-const InOut = require('../../lib/jsunit.js')
-let Input = InOut.Input
-let Output = InOut.Output
+const JSUnit = require('../../src/jsunit.js')
+let Input = JSUnit.Input
+let Output = JSUnit.Output
 
-let State = InOut.State
-let Button = InOut.Button
+let State = JSUnit.State
+let Button = JSUnit.Button
 
 const SerialPort = require('serialport')
 
diff --git a/modules/hardware/atkstepper.js b/modules/hardware/atkstepper.js
index 1a9fdf7a29ec01dbe8e86f574ba6a2df251dd615..d819b3482f1bc16faefb214e26bf72d4df09fe2a 100644
--- a/modules/hardware/atkstepper.js
+++ b/modules/hardware/atkstepper.js
@@ -1,16 +1,16 @@
 // boilerplate atkapi header
-const InOut = require('../../lib/jsunit.js')
-let Input = InOut.Input
-let Output = InOut.Output
+const JSUnit = require('../../src/jsunit.js')
+let Input = JSUnit.Input
+let Output = JSUnit.Output
 
-let State = InOut.State
-let Button = InOut.Button
+let State = JSUnit.State
+let Button = JSUnit.Button
 
 const MJS = require('mathjs')
-const DCRT = require('../../lib/cartesian.js')
+const DCRT = require('../../src/cartesian.js')
 
-const Hardware = require('../../lib/atkunit.js')
-const PCKT = require('../../lib/packets.js')
+const Hardware = require('../../src/atkunit.js')
+const PCKT = require('../../src/packets.js')
 
 function Stepper() {
 
diff --git a/modules/hardware/webcam.js b/modules/hardware/webcam.js
index bce6df3c338ba9c1d361e87ba18cbb64b6282e58..3ac82a73e2d2246598afae35348bf69d0b002537 100644
--- a/modules/hardware/webcam.js
+++ b/modules/hardware/webcam.js
@@ -1,9 +1,9 @@
 // boilerplate atkapi header
-const InOut = require('../../lib/jsunit.js')
-let Input = InOut.Input
-let Output = InOut.Output
-let State = InOut.State
-let Button = InOut.Button
+const JSUnit = require('../../src/jsunit.js')
+let Input = JSUnit.Input
+let Output = JSUnit.Output
+let State = JSUnit.State
+let Button = JSUnit.Button
 
 var NodeWebcam = require("node-webcam")
 
diff --git a/modules/motion/planner.js b/modules/motion/planner.js
index 8f2f19cd5a66e582fc04401bfc48ab20a5d0d37b..04e088a6077bfa0321eee733682601eb24832564 100644
--- a/modules/motion/planner.js
+++ b/modules/motion/planner.js
@@ -1,12 +1,12 @@
 // boilerplate atkapi header
-const InOut = require('../../lib/jsunit.js')
-let Input = InOut.Input
-let Output = InOut.Output
-let State = InOut.State
-let Button = InOut.Button
+const JSUnit = require('../../src/jsunit.js')
+let Input = JSUnit.Input
+let Output = JSUnit.Output
+let State = JSUnit.State
+let Button = JSUnit.Button
 
 // descartes, to you 
-const DCRT = require('../../lib/cartesian.js')
+const DCRT = require('../../src/cartesian.js')
 const MJS = require('mathjs')
 
 // planner consumes target moves (i.e. segments having uniform speed throughout)
diff --git a/modules/motion/rawmove.js b/modules/motion/rawmove.js
index 18d244c7a2d4347fed33105d01cd3002609092ce..3a25b1d3015651a8e500d7216499507945b7d808 100644
--- a/modules/motion/rawmove.js
+++ b/modules/motion/rawmove.js
@@ -1,9 +1,9 @@
 // boilerplate atkapi header
-const InOut = require('../../lib/jsunit.js')
-let Input = InOut.Input
-let Output = InOut.Output
-let State = InOut.State
-let Button = InOut.Button
+const JSUnit = require('../../src/jsunit.js')
+let Input = JSUnit.Input
+let Output = JSUnit.Output
+let State = JSUnit.State
+let Button = JSUnit.Button
 
 function RawMove() {
 	var rawmove = {
diff --git a/modules/parsing/gcode.js b/modules/parsing/gcode.js
index ae2ecb824e1847b9d848b92e11e4f08b04884392..ac118fb9fad30312054152a45a1bb4a984f32e0f 100644
--- a/modules/parsing/gcode.js
+++ b/modules/parsing/gcode.js
@@ -1,8 +1,8 @@
 // boilerplate atkapi header
-const InOut = require('../../lib/jsunit.js')
-let Input = InOut.Input
-let Output = InOut.Output
-let State = InOut.State
+const JSUnit = require('../../src/jsunit.js')
+let Input = JSUnit.Input
+let Output = JSUnit.Output
+let State = JSUnit.State
 
 function Gcode() {
 
diff --git a/modules/ui/button.js b/modules/ui/button.js
index d8435aa67b7f57e49eec959f99d81851e199c6d9..a86e9acd4156e88bfd25e44d579f28f90285c2b5 100644
--- a/modules/ui/button.js
+++ b/modules/ui/button.js
@@ -1,9 +1,9 @@
 // boilerplate atkapi header
-const InOut = require('../../lib/jsunit.js')
-let Input = InOut.Input
-let Output = InOut.Output
-let State = InOut.State
-let Button = InOut.Button
+const JSUnit = require('../../src/jsunit.js')
+let Input = JSUnit.Input
+let Output = JSUnit.Output
+let State = JSUnit.State
+let Button = JSUnit.Button
 
 // a constructor, a fn, a javascript mess
 function uiButton() {
diff --git a/modules/ui/multiline.js b/modules/ui/multiline.js
index 376f7ae1aaecce61503eaea056c58ae52ee93b78..da519c5bee14ce19c20ba068227011cf251949af 100644
--- a/modules/ui/multiline.js
+++ b/modules/ui/multiline.js
@@ -1,11 +1,9 @@
 // boilerplate atkapi header
-const InOut = require('../../lib/jsunit.js')
-let Input = InOut.Input
-let Output = InOut.Output
-let State = InOut.State
+const JSUnit = require('../../src/jsunit.js')
+let Input = JSUnit.Input
+let Output = JSUnit.Output
 
-let MultiLine = InOut.MultiLine
-let Button = InOut.Button
+let State = JSUnit.State
 
 const fs = require('fs')
 
@@ -22,9 +20,8 @@ function MultiLineIn() {
 
     multilinein.state = State()
     // alias !
-    var state = multilinein.state
-    state.load = Button('LOAD', onLoadFile)
-    state.thru = Button('WHAM', lineThru)
+    state.load = State.newButton('--', onLoadFile)
+    state.thru = Button('THRU', lineThru)
     state.previously = MultiLine('lines complete', 11)
     state.now = MultiLine('line just out', 1)
     state.incoming = MultiLine('future lines', 36)
@@ -94,6 +91,8 @@ function MultiLineIn() {
 
     multilinein.load = onLoadFile
 
+    onLoadFile('./files/dogbone.gcode')
+
     function onExternalLine(str) {
         // push new str to bottom of queue
     }
diff --git a/modules/ui/number.js b/modules/ui/number.js
index bff0d2b803f7c2679e24b65c121afdfef1367de7..c0c17a1b3d79893765689c4af58354b3942a406f 100644
--- a/modules/ui/number.js
+++ b/modules/ui/number.js
@@ -1,9 +1,9 @@
 // boilerplate rndmc header
-const InOut = require('../../lib/jsunit.js')
-let Input = InOut.Input
-let Output = InOut.Output
-let State = InOut.State
-let Button = InOut.Button
+const JSUnit = require('../../src/jsunit.js')
+let Input = JSUnit.Input
+let Output = JSUnit.Output
+let State = JSUnit.State
+let Button = JSUnit.Button
 
 // a constructor, a fn, a javascript mess
 function uiNum() {
diff --git a/modules/ui/stest.js b/modules/ui/stest.js
new file mode 100644
index 0000000000000000000000000000000000000000..d44751aed2443012477f71a98ed020271dc74fed
--- /dev/null
+++ b/modules/ui/stest.js
@@ -0,0 +1,61 @@
+const JSUnit = require('../../src/jsunit.js')
+let Input = JSUnit.Input
+let Output = JSUnit.Output
+let State = JSUnit.State
+
+// interface elements
+const JSUI = require('../../src/jsui.js')
+let UI = JSUI.UI 
+
+function STest() {
+	var stest = {
+		description:{
+			name: 'test-for-state',
+			alt: 'a description'
+		}
+	}
+
+	// state is numbers, strings, arrays of either, booleans
+	// booleans are buttons 
+	stest.state = State() 
+	var state = stest.state 
+	state.num = 12 
+	state.str = 'one string'
+
+	state.arr = new Array()
+	state.arr.push(12)
+	state.arr.push(24)
+
+	state.strarr = new Array()
+	state.strarr.push('str1')
+	state.strarr.push('str2')
+
+	state.boolean = false 
+
+	stest.inputs = {
+		inp: Input('any', onInp)
+	}
+
+	function onInp(evt){
+		console.log('ex input', evt)
+	}
+
+	stest.outputs = {
+		outp: Output('any')
+	}
+
+
+	// other items for interaction are explicitly UI 
+	stest.ui = UI() 
+	var ui = stest.ui 
+	ui.addElement('btnex', './ui/uiButton.js', onButtonData)
+
+	function onButtonData(evt){
+		console.log('button change evt', evt)
+		ui.btnex.isPressed = true 
+	}
+
+	return stest
+}
+
+module.exports = STest 
\ No newline at end of file
diff --git a/modules/ui/terminal.js b/modules/ui/terminal.js
index 62a3a404869d440ab8e6467aaf3ba8f4e664d1a3..c153aa9a59fd4e994ec1c9ea0d569dcf26be736a 100644
--- a/modules/ui/terminal.js
+++ b/modules/ui/terminal.js
@@ -1,8 +1,8 @@
 // boilerplate atkapi header
-const InOut = require('../../lib/jsunit.js')
-let Input = InOut.Input
-let Output = InOut.Output
-let State = InOut.State
+const JSUnit = require('../../src/jsunit.js')
+let Input = JSUnit.Input
+let Output = JSUnit.Output
+let State = JSUnit.State
 
 // a constructor, a fn, a javascript mess
 function Terminal() {
diff --git a/modules/util/andgate.js b/modules/util/andgate.js
index 6f1b4965db0a755a659652df0ba71276a62c8418..0ad34185ab53418225c516f8fb6fca72378fc598 100644
--- a/modules/util/andgate.js
+++ b/modules/util/andgate.js
@@ -1,9 +1,9 @@
 // boilerplate atkapi header
-const InOut = require('../../lib/jsunit.js')
-let Input = InOut.Input
-let Output = InOut.Output
-let State = InOut.State
-let Button = InOut.Button
+const JSUnit = require('../../src/jsunit.js')
+let Input = JSUnit.Input
+let Output = JSUnit.Output
+let State = JSUnit.State
+let Button = JSUnit.Button
 
 // a constructor, a fn, a javascript mess
 function AndFlowControl() {
diff --git a/modules/util/delay.js b/modules/util/delay.js
index fd7b9f06b7cd7b7ce3088b74baae2a7c28f0a2dc..b20c87c1b4da5063d03c35185625863b41311951 100644
--- a/modules/util/delay.js
+++ b/modules/util/delay.js
@@ -1,6 +1,6 @@
 // boilerplate atkapi header
 // boilerplate header
-const JSUnit = require('../../lib/jsunit.js')
+const JSUnit = require('../../src/jsunit.js')
 let Input = JSUnit.Input
 let Output = JSUnit.Output
 let State = JSUnit.State
diff --git a/modules/util/gate.js b/modules/util/gate.js
index ea6e1c9e53a34afef3472d1e87895935dc347b90..cada67a4316aea7b16c783645bcf31b8b05ef37c 100644
--- a/modules/util/gate.js
+++ b/modules/util/gate.js
@@ -1,5 +1,5 @@
 // boilerplate header
-const JSUnit = require('../../lib/jsunit.js')
+const JSUnit = require('../../src/jsunit.js')
 let Input = JSUnit.Input
 let Output = JSUnit.Output
 let State = JSUnit.State
diff --git a/modules/util/log.js b/modules/util/log.js
index 69c4f60f2306c043a131e8ab86c2e087ae4bf4a7..eb9ad295a1f96feb084b940eab3769be72ddec5b 100644
--- a/modules/util/log.js
+++ b/modules/util/log.js
@@ -1,5 +1,5 @@
 // boilerplate header
-const JSUnit = require('../../lib/jsunit.js')
+const JSUnit = require('../../src/jsunit.js')
 let Input = JSUnit.Input
 let Output = JSUnit.Output
 let State = JSUnit.State
diff --git a/programs.js b/programs.js
index 9582a6ec53a98a1b3612f50191257d6460b443b9..97121d8c5402aef1558711ecc3eb8925138ec48a 100644
--- a/programs.js
+++ b/programs.js
@@ -2,9 +2,11 @@ const fs = require('fs')
 
 const Reps = require('./reps.js')
 
-const JSUnit = require('./lib/jsunit.js')
+const JSUnit = require('./src/jsunit.js')
 let isStateKey = JSUnit.isStateKey
 
+var socket = {}
+
 function newProgram(name) {
     var program = {
         description: {
@@ -20,6 +22,7 @@ function newProgram(name) {
 function loadModuleFromSource(program, path, id) {
     // source -> heap
     if (fs.existsSync(path)) {
+        // compile a new object based on definition in path
         var src = require(path)
         var mod = new src()
 
@@ -41,47 +44,41 @@ function loadModuleFromSource(program, path, id) {
         // add to program object 
         program.modules[mod.description.id] = mod
 
-        /* ---------------------------------- */
-        // WARN! Corner Case should Go Away or Improve at next spiral 
-        if (mod.description.isLink) {
-            for (mdlName in program.modules) {
-                if (program.modules[mdlName].description.isLink) {
-                    console.log("PRGMEM ONLY BIG ENOUGH FOR ONE LINK")
-                    //process.exit()
-                }
-            }
-        }
-        // end corner case code 
-        /* ---------------------------------- */
-
         // input need references for later hookup
         for (key in mod.inputs) {
             mod.inputs[key].parentId = mod.description.id
             mod.inputs[key].key = key
         }
 
-        // state updating, begs for update 
+        // state items get wrapped in a getter / setter
+        // so that changes from internal modules can
+        // push to UI 
+        mod.state.init(mod.description.id, socket)
+
+        mod.ui.init(mod.description.id, socket)
+        
+        /*
         for (key in mod.state) {
-            if (key == 'onChange' | key == 'emitChange' | key == 'emitters') {
-                //console.log('rolling past change fn')
-            } else {
-                mod.state['_' + key] = mod.state[key]
-                mod.state[key] = {}
+            if(isStateKey(key)){
                 writeStateObject(mod, key)
             }
         }
+        */
+
+        console.log('ADDING', mod.description.id, 'to', program.description.id)
 
-        if (program.description.id == null) {
-            if (program.description.name == null) {
-                if (program.description == null) {
-                    program.description = {}
+        /* ---------------------------------- */
+        // WARN! Corner Case should Go Away or Improve at next spiral 
+        if (mod.description.isLink) {
+            for (mdlName in program.modules) {
+                if (program.modules[mdlName].description.isLink) {
+                    console.log("PRGMEM ONLY BIG ENOUGH FOR ONE LINK")
+                    //process.exit()
                 }
-                program.description.name = 'unnamed program'
             }
-            program.description.id = program.description.name + '-' + 0
         }
-
-        console.log('ADDING', mod.description.id, 'to', program.description.id)
+        // end corner case code 
+        /* ---------------------------------- */
 
         /* ---------------------------------- */
         // WARN! Corner Case should Go Away or Improve at next spiral 
@@ -115,17 +112,16 @@ function loadModuleFromSource(program, path, id) {
     }
 }
 
+/*
 function writeStateObject(mod, key) {
+    mod.state['_' + key] = mod.state[key]
+    mod.state[key] = {}
+
     Object.defineProperty(mod.state, key, {
         set: function(x) {
             // update internal value 
             this['_' + key] = x
             // console.log('SET', key, this['_' + key])
-            // push to internal state change handler
-            // let's call emitChange from the server-side ...
-            // so that we don't get into any heavy VIR
-            // when we change it within the module 
-            // this.emitChange(key)
             // push to external view
             if (socket) {
                 pushState(mod, key)
@@ -134,11 +130,14 @@ function writeStateObject(mod, key) {
     })
     Object.defineProperty(mod.state, key, {
         get: function() {
+            console.log('KEY', key)
+            console.log("IS", this['_' + key])
             //console.log('GET', key, this['_' + key])
             return this['_' + key]
         }
     })
 }
+*/
 
 function removeModuleFromProgram(program, id) {
     // this simple? 
@@ -159,19 +158,10 @@ EXTERNAL HOOKS
 */
 
 function assignSocket(sckt) {
-    socket = sckt
+    // we can pass this object 'down' by reference, once it loads 
+    socket.send = sckt.send 
 }
 
-function pushState(mod, key) {
-    var data = {
-        id: mod.description.id,
-        key: key,
-        val: mod.state[key]
-    }
-    socket.send('put state change', data)
-}
-
-
 function saveProgram(prgmem, path) {
     // ok, and we're interested in just copying the relevant things ... 
     var svprgmem = {
@@ -195,7 +185,6 @@ function saveProgram(prgmem, path) {
     console.log('PROGRAM SAVED AT', path)
 }
 
-
 function openProgram(path) {
     var program = {}
 
@@ -263,7 +252,7 @@ function openProgram(path) {
                         // TODO: states: sometimes we load, we want to run the change emitter ... sometimes we don't
                         // what choice ? 
                         mdl.state['_' + key] = mdlRep.state[key]
-                        mdl.state.emitChange('route')
+                        mdl.state.emitUIChange('route')
                     } else {
                         mdl.state['_' + key] = mdlRep.state[key]
                     }
diff --git a/reps.js b/reps.js
index 853de088575b62d3e43b9575dbf45448cf687e16..9b7f466705c222a5d7ac3df5e49dec0daf1bfe44 100644
--- a/reps.js
+++ b/reps.js
@@ -1,29 +1,16 @@
-const JSUnit = require('./lib/jsunit.js')
+const JSUnit = require('./src/jsunit.js')
 let isStateKey = JSUnit.isStateKey
 
+const JSUI = require('./src/jsui.js')
+let isUiKey = JSUI.isUiKey
+
 function makeRepFromModule(mdl) {
     // rep != mdl 
     // rep is rep of mdl 
-    var rep = {
-        description: {
-            id: mdl.description.id,
-            name: mdl.description.name,
-            alt: mdl.description.alt,
-            path: mdl.description.path
-        }
-    }
-
-    // yikes tho, corner cases should be easier ... 
-    if(mdl.description.isHardware){
-        rep.description.isHardware = true
-    }
 
-    if(mdl.description.isLink){
-        rep.description.isLink = true 
-    }
-
-    if(mdl.description.position){
-        rep.description.position = mdl.description.position
+    // deep copy description 
+    var rep = {
+        description: JSON.parse(JSON.stringify(mdl.description))
     }
 
     // TODO: making rep. of input / output should be a f'n of that object ...
@@ -56,6 +43,15 @@ function makeRepFromModule(mdl) {
         }
     }
 
+    rep.ui = {}
+    for(key in mdl.ui){
+        if(isUiKey(key)){
+            rep.ui[key] = {} 
+            rep.ui[key].type = mdl.ui[key].type 
+            rep.ui[key].clientPath = mdl.ui[key].clientPath  
+        }
+    }
+
     return rep
 }
 
diff --git a/main.js b/rundmc.js
similarity index 88%
rename from main.js
rename to rundmc.js
index 20d41bd1a242e9f4cdc0c3f30332cb0eeca21971..e9c38aaf1ec31d16ea9e6a8efd977a7b124d52a7 100644
--- a/main.js
+++ b/rundmc.js
@@ -1,6 +1,7 @@
 //
 //
 // new node controller / HEAP 
+// reconfigurable numeric dataflow machine controller / RNDMC
 //
 // main.js
 //
@@ -30,6 +31,11 @@ const Programs = require('./programs.js')
 // the program object: real simple, just has a description, and a 'modules' 
 var program = Programs.new('new program')
 
+var stest = Programs.loadModuleFromSource(program, './modules/ui/stest.js')
+
+var rep = Reps.makeFromModule(stest)
+console.log('rep', rep)
+
 /* example program-like-an-api 
 // load some modules
 var multiline = Programs.loadModuleFromSource(program, './modules/ui/multiline.js')
diff --git a/src/atkroute.js b/src/atkroute.js
new file mode 100644
index 0000000000000000000000000000000000000000..02c2ba4f2b66c4651fb6de66b3b243f2b1a2483d
--- /dev/null
+++ b/src/atkroute.js
@@ -0,0 +1,37 @@
+// object to extend for things-that-are-hardware
+
+// and pass f'n to call on received messages
+function ATKRoute(route, calls) {
+    var atkroute = {
+        isAtkRoute: true,
+        link: null, // pls, deliver 2 me a hw outlet 
+        route: route,
+        calls: {}
+    }
+
+    atkroute.send = function(msg) {
+        if (this.link != null) {
+            // CHECKS and append route, then send 
+            this.link.send(msg, this)
+        } else {
+            console.log("NO LINK NO SEND")
+        }
+    }
+
+    atkroute.subscribe = function(key, callback){
+        this.calls[key.toString()] = callback
+    }
+
+    atkroute.onMessage = function(msg){
+        // one key at a time, for now
+        var key = msg[0].toString()
+        // *could* slice the key out at this point, but nah 
+        if(this.calls[key] != null){
+            this.calls[key](msg)
+        }
+    }
+
+    return atkroute
+}
+
+module.exports = ATKRoute
\ No newline at end of file
diff --git a/src/atkunit.js b/src/atkunit.js
new file mode 100644
index 0000000000000000000000000000000000000000..529bc8b1a6bff81c8d102111299ebf555a97b875
--- /dev/null
+++ b/src/atkunit.js
@@ -0,0 +1,59 @@
+// software object for reciprocal hardware object 
+
+// boilerplate atkapi header
+const JSUnit = require('./jsunit.js')
+let Input = JSUnit.Input
+let Output = JSUnit.Output
+let State = JSUnit.State
+let Button = JSUnit.Button
+
+let ATKRoute = require('./atkroute.js')
+
+function Hardware(){
+	var hardware = {
+		description:{
+			name: 'hardwareUnit',
+			alt: 'software representation of networked hardware object',
+			isHardware: true 
+		},
+		route: ATKRoute('0,0')
+	}
+
+	hardware.state = State()
+	var state = hardware.state 
+
+	state.reset = Button('reset hardware', onReset)
+
+	state.test = Button('test network', onNetworkTest)
+	state.message = 'click above to test network'
+	state.route = '0,0' // default  
+
+	state.onChange('route', function(){
+		hardware.route.route = state.route 
+	})
+
+	function onReset(){
+		var rstpck = new Array()
+		rstpck.push(128)
+		state.message = 'reset command issued'
+		hardware.route.send(rstpck)
+	}
+
+	function onNetworkTest(){
+		var tstpck = new Array()
+		tstpck.push(127)
+		state.message = 'test packet out'
+		hardware.route.send(tstpck)
+	}
+
+	hardware.route.subscribe(127, testReturn)
+
+	function testReturn(msg){
+		state.message = 'test OK'
+		console.log('test returns with msg', msg)
+	}
+
+	return hardware
+}
+
+module.exports = Hardware
\ No newline at end of file
diff --git a/src/cartesian.js b/src/cartesian.js
new file mode 100644
index 0000000000000000000000000000000000000000..c6255fedb01e0b9f73971aa493d300c1dd69a484
--- /dev/null
+++ b/src/cartesian.js
@@ -0,0 +1,28 @@
+function distance(p1, p2) {
+    // takes p1, p2 to be arrays of same length
+    // computes cartesian distance
+    var sum = 0
+    for (var i = 0; i < p1.length; i++) {
+        sum += Math.pow((p1[i] - p2[i]), 2)
+    }
+    return Math.sqrt(sum)
+}
+
+function length(v) {
+    // length of vector
+    var sum = 0
+    for (var i = 0; i < v.length; i++) {
+        sum += Math.pow(v[i], 2)
+    }
+    return Math.sqrt(sum)
+}
+
+function degrees(rad){
+    return rad * (180 / Math.PI)
+}
+
+module.exports = {
+    distance: distance,
+    length: length,
+    degrees: degrees
+}
\ No newline at end of file
diff --git a/src/jsui.js b/src/jsui.js
new file mode 100644
index 0000000000000000000000000000000000000000..46f78466e8770ca0f9aada2cafe82e66e75c1395
--- /dev/null
+++ b/src/jsui.js
@@ -0,0 +1,40 @@
+function UI() {
+    var ui = {}
+
+    ui.addElement = function(keyName, srcPath, callback){
+    	var src = require(srcPath)
+    	ui[keyName] = new src() 
+    	ui[keyName].callback = callback 
+    }
+
+    ui.init = function(parentModId, socket){
+    	// get hookups from top level program 
+        this.parentId = parentModId
+        this.socket = socket
+        // and wrap state objects in getters / setters 
+        for (key in this) {
+            if (isUiKey(key)) {
+            	// see if they have init functions?
+            }
+        }
+    }
+
+    return ui
+}
+
+function isUiKey(key) {
+    if (key == 'parentId' || key == 'socket' || key == 'init' || key == 'addElement' || key == 'pushToUI' || key == 'emitters') {
+        return false
+    } else {
+        return true
+    }
+}
+
+module.exports = {
+	UI: UI,
+	isUiKey: isUiKey
+}
+
+/* bless u mdn 
+https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement
+*/
\ No newline at end of file
diff --git a/src/jsunit.js b/src/jsunit.js
new file mode 100644
index 0000000000000000000000000000000000000000..90cbec4f5996afd90739b7fd2775eb51bf786e14
--- /dev/null
+++ b/src/jsunit.js
@@ -0,0 +1,151 @@
+// event system to include type-checking etc
+// dataflow types for javascript objects ... 
+
+function Input(type, fn) {
+    var input = {
+        accepts: type,
+        fn: fn
+    }
+
+    return input
+}
+
+function Output(type) {
+    var output = {
+        emits: type
+    }
+
+    output.calls = new Array()
+
+    output.attach = function(input) {
+        this.calls.push(input)
+    }
+
+    output.isLinked = function(input) {
+        // return true if already hooked up 
+        if (this.calls.includes(input)) {
+            return true
+        } else {
+            return false
+        }
+    }
+
+    output.remove = function(input) {
+        if (!this.isLinked(input)) {
+            console.log('attempt to rm input that is not attached')
+            return false
+        } else {
+            this.calls.splice(this.calls.indexOf(input), 1)
+        }
+    }
+
+    output.removeAllLinks = function() {
+        this.calls = []
+    }
+
+    output.checkLinks = function(id) {
+        console.log('checking links', this)
+        for (index in this.calls) {
+            if (this.calls[index].parentId == id) {
+                console.log('popping null entry from', this.calls)
+                this.calls.splice(index, 1)
+                console.log('new record', this.calls)
+            } else {
+                // all good 
+            }
+        }
+    }
+
+    output.emit = function(data) {
+        if (this.calls.length == 0) {
+            console.log('no inputs bound to this output')
+        } else {
+            for (index in this.calls) {
+                this.calls[index].fn(JSON.parse(JSON.stringify(data)))
+            }
+        }
+    }
+
+    return output
+}
+
+function State() {
+    var state = {}
+
+    state.emitters = {}
+    state.parentId = null
+    state.socket = null
+
+    // called when change from UI 
+    state.onUIChange = function(item, fn) {
+        this.emitters[item] = fn
+    }
+
+    state.emitUIChange = function(item) {
+        if (this.emitters[item] != null) {
+            this.emitters[item]()
+        }
+    }
+
+    state.pushToUI = function(key) {
+        if (this.socket) {
+            var data = {
+                id: this.parentId,
+                key: key,
+                val: this[key]
+            }
+            this.socket.send('put state change', data)
+        } else {
+            console.log("ERR on state update to UI, socket is", this.socket)
+        }
+    }
+
+    state.init = function(parentModId, socket) {
+        // get hookups from top level program 
+        this.parentId = parentModId
+        this.socket = socket
+        // and wrap state objects in getters / setters 
+        for (key in this) {
+            if (isStateKey(key)) {
+                this['_' + key] = this[key]
+                this[key] = {}
+                writeStateGetterSetter(this, key)
+            }
+        }
+    }
+
+    return state
+}
+
+function writeStateGetterSetter(state, key) {
+    Object.defineProperty(state, key, {
+        set: function(x) {
+            // update internal value 
+            state['_' + key] = x
+            // console.log('SET', key, this['_' + key])
+            // push to external view
+            state.pushToUI(key)
+        }
+    })
+    Object.defineProperty(state, key, {
+        get: function() {
+            //console.log('GET', key, this['_' + key])
+            return state['_' + key]
+        }
+    })
+}
+
+function isStateKey(key) {
+    if (key.indexOf('_') == 0 || key == 'parentId' || key == 'socket' || key == 'init' || key == 'pushToUI' || key == 'emitters' || key == 'onUIChange' || key == 'emitUIChange' ) {
+        return false
+    } else {
+        return true
+    }
+}
+
+module.exports = {
+    Input: Input,
+    Output: Output,
+    State: State,
+    isStateKey: isStateKey
+}
\ No newline at end of file
diff --git a/src/packets.js b/src/packets.js
new file mode 100644
index 0000000000000000000000000000000000000000..ad63359112614e31333d930278a84e9f2ee26a45
--- /dev/null
+++ b/src/packets.js
@@ -0,0 +1,24 @@
+function pack32(val) {
+    var pack = new Array();
+    pack[0] = (val >> 24) & 255;
+    pack[1] = (val >> 16) & 255;
+    pack[2] = (val >> 8) & 255;
+    pack[3] = val & 255;
+
+    return pack;
+}
+
+function unPack32(arr){
+	if(arr.length == 4){
+		var unPacked = arr[0] << 24 | arr[1] << 16 | arr[2] << 8 | arr[3] 
+		return unPacked
+	} else {
+		console.log("ERR: arr > 4 at unPack32", arr)
+	}
+	
+}
+
+module.exports = {
+	pack32: pack32,
+	unPack32: unPack32
+}
\ No newline at end of file
diff --git a/src/ui/uiButton.js b/src/ui/uiButton.js
new file mode 100644
index 0000000000000000000000000000000000000000..60c9e2e9a4ba5ca5fd745b47ff1e251e6dfc7df5
--- /dev/null
+++ b/src/ui/uiButton.js
@@ -0,0 +1,11 @@
+function UIButton() {
+	var uiButton = {
+		type: 'button',
+		clientPath: 'ui/uiButton.js',
+		isPressed: false
+	}
+
+	return uiButton 
+}
+
+module.exports = UIButton 
\ No newline at end of file
diff --git a/views.js b/views.js
index 32d1138a1c6072d6673738eed3b7d3cea20e223a..d77ea9dcd0b7730ee75de6ad00ded0c840be619a 100644
--- a/views.js
+++ b/views.js
@@ -32,6 +32,11 @@ function startHttp() {
         res.sendFile(__dirname + '/client/' + req.params.file)
     })
 
+    app.get('/ui/:file', (req, res) => {
+        console.log('client req ui', req.params.file)
+        res.sendFile(__dirname + '/client/ui/' + req.params.file)
+    })
+
     // through this window
     http.listen(8080, () => {
         console.log('RNDMC is listening on localhost:8080')
@@ -100,8 +105,8 @@ function socketRecv(evt) {
             uiRequestNewModule(data)
             break
         case 'remove module':
-            uiRequestRemoveModule(data) 
-            break 
+            uiRequestRemoveModule(data)
+            break
         case 'put state change':
             uiRequestStateChange(data)
             break
@@ -227,19 +232,19 @@ function uiRequestNewModule(data) {
     // bit of a mess to pick out the last entered module 
     var keys = Object.keys(program.modules)
     var latest = keys[keys.length - 1]
-    if(program.modules[latest].description.isLink && data != './modules/hardware/atkseriallink.js'){
+    if (program.modules[latest].description.isLink && data != './modules/hardware/atkseriallink.js') {
         // we just added hardware, so added a link, so we've added two 
         // just burn it down 
         socketSend('restart', '')
     }
     // TODO: questionable init - here and in loadProgram, should be better handled across board 
-    if(program.modules[latest].init != null){
+    if (program.modules[latest].init != null) {
         program.modules[latest].init()
     }
     socketSend('put module', Reps.makeFromModule(program.modules[keys[keys.length - 1]]))
 }
 
-function uiRequestRemoveModule(data){
+function uiRequestRemoveModule(data) {
     console.log('UI REQUEST TO REMOVE MODULE', data.id)
     Programs.removeModule(program, data.id)
     socketSend('restart', '')
@@ -250,22 +255,14 @@ function uiRequestStateChange(data) {
 
     var mdlState = program.modules[data.id].state
 
-    if (mdlState[data.key]) {
-        switch (mdlState[data.key].type) {
-            case 'button': 
-                mdlState[data.key].onClick()
-                break
-            case 'multiline':
-                mdlState[data.key].value = data.val 
-                mdlState.emitChange(data.key)
-                break
-            default:
-                mdlState[data.key] = data.val 
-                mdlState.emitChange(data.key)
-                break
-        }
+    if (mdlState[data.key] != null) {
+        mdlState[data.key] = data.val
+        mdlState.emitUIChange(data.key)
     } else {
-        console.log("ERR no state key,", key, "found here", data)
+        console.log("ERR no state key,", data.key, "found here", data)
+        console.log("THE KEY", data.key)
+        console.log("THE MDL", mdlState)
+        console.log("THE STATE", mdlState[data.key])
     }
 }
 
@@ -280,7 +277,7 @@ function uiRequestLinkChange(data) {
     var toMdl = program.modules[toId]
 
     // if it's already attached, we rm
-    if(fromMdl.outputs[outputName].isLinked(toMdl.inputs[inputName])){
+    if (fromMdl.outputs[outputName].isLinked(toMdl.inputs[inputName])) {
         fromMdl.outputs[outputName].remove(toMdl.inputs[inputName])
     } else {
         fromMdl.outputs[outputName].attach(toMdl.inputs[inputName])