From 1727eb621bc23d604109337505738f7f7e0721de Mon Sep 17 00:00:00 2001
From: Mitchell Pomery <bob_george33@hotmail.com>
Date: Sun, 9 Feb 2014 13:23:19 +0800
Subject: [PATCH] Start of a web interface

Webby
---
 BlinkenLights.ino                |   3 +-
 web/css/simple-slider-volume.css |  48 ++++
 web/css/simple-slider.css        |  55 +++++
 web/js/simple-slider.coffee      | 361 ++++++++++++++++++++++++++++++
 web/js/simple-slider.js          | 363 +++++++++++++++++++++++++++++++
 web/js/simple-slider.min.js      |  11 +
 web/web.php                      |  91 ++++++++
 7 files changed, 931 insertions(+), 1 deletion(-)
 create mode 100644 web/css/simple-slider-volume.css
 create mode 100644 web/css/simple-slider.css
 create mode 100644 web/js/simple-slider.coffee
 create mode 100644 web/js/simple-slider.js
 create mode 100644 web/js/simple-slider.min.js
 create mode 100644 web/web.php

diff --git a/BlinkenLights.ino b/BlinkenLights.ino
index 94f2e5b..f326fc0 100644
--- a/BlinkenLights.ino
+++ b/BlinkenLights.ino
@@ -258,7 +258,8 @@ char *url_tail, bool tail_complete) {
   char name[NAMELEN];
   char value[VALUELEN];
   
-  server.httpSuccess();
+  //server.print("Access-Control-Allow-Origin: *");
+  server.httpSuccess("application/json", "Access-Control-Allow-Origin: *");
   // Kill the connection before doing anything if all they want is head
   if (type == WebServer::HEAD) {
     return;
diff --git a/web/css/simple-slider-volume.css b/web/css/simple-slider-volume.css
new file mode 100644
index 0000000..d8a876b
--- /dev/null
+++ b/web/css/simple-slider-volume.css
@@ -0,0 +1,48 @@
+.slider-volume {
+  width: 300px;
+}
+
+.slider-volume > .dragger {
+	width: 16px;
+	height: 16px;
+	margin: 0 auto;
+  border: 1px solid rgba(255,255,255,0.6);
+
+  -moz-box-shadow: 0 0px 2px 1px rgba(0,0,0,0.5), 0 2px 5px 2px rgba(0,0,0,0.2);
+  -webkit-box-shadow: 0 0px 2px 1px rgba(0,0,0,0.5), 0 2px 5px 2px rgba(0,0,0,0.2);
+  box-shadow: 0 0px 2px 1px rgba(0,0,0,0.5), 0 2px 5px 2px rgba(0,0,0,0.2);
+
+	-moz-border-radius: 10px;
+	-webkit-border-radius: 10px;
+	border-radius: 10px;
+
+  background: #c5c5c5;
+  background: -moz-linear-gradient(90deg, rgba(180,180,180,1) 20%, rgba(230,230,230,1) 50%, rgba(180,180,180,1) 80%);
+	background:	-webkit-radial-gradient(  50%   0%,  12% 50%, hsla(0,0%,100%,1) 0%, hsla(0,0%,100%,0) 100%),
+  	          -webkit-radial-gradient(  50% 100%, 12% 50%, hsla(0,0%,100%,.6) 0%, hsla(0,0%,100%,0) 100%),
+              -webkit-radial-gradient(	50% 50%, 200% 50%, hsla(0,0%,90%,1) 5%, hsla(0,0%,85%,1) 30%, hsla(0,0%,60%,1) 100%);
+}
+
+.slider-volume > .track, .slider-volume > .highlight-track {
+  height: 11px;
+
+  background: #787878;
+  background: -moz-linear-gradient(top, #787878, #a2a2a2);
+  background: -webkit-linear-gradient(top, #787878, #a2a2a2);
+  background: linear-gradient(top, #787878, #a2a2a2);
+
+  -moz-box-shadow: inset 0 2px 5px 1px rgba(0,0,0,0.15), 0 1px 0px 0px rgba(230,230,230,0.9), inset 0 0 1px 1px rgba(0,0,0,0.2);
+  -webkit-box-shadow: inset 0 2px 5px 1px rgba(0,0,0,0.15), 0 1px 0px 0px rgba(230,230,230,0.9), inset 0 0 1px 1px rgba(0,0,0,0.2);
+  box-shadow: inset 0 2px 5px 1px rgba(0,0,0,0.15), 0 1px 0px 0px rgba(230,230,230,0.9), inset 0 0 1px 1px rgba(0,0,0,0.2);
+
+  -moz-border-radius: 5px;
+  -webkit-border-radius: 5px;
+  border-radius: 5px;
+}
+
+.slider-volume > .highlight-track {
+  background-color: #c5c5c5;
+  background: -moz-linear-gradient(top, #c5c5c5, #a2a2a2);
+  background: -webkit-linear-gradient(top, #c5c5c5, #a2a2a2);
+  background: linear-gradient(top, #c5c5c5, #a2a2a2);
+}
diff --git a/web/css/simple-slider.css b/web/css/simple-slider.css
new file mode 100644
index 0000000..ca9bf6b
--- /dev/null
+++ b/web/css/simple-slider.css
@@ -0,0 +1,55 @@
+.slider {
+  width: 300px;
+}
+
+.slider > .dragger {
+  background: #8DCA09;
+  background: -webkit-linear-gradient(top, #8DCA09, #72A307);
+  background: -moz-linear-gradient(top, #8DCA09, #72A307);
+  background: linear-gradient(top, #8DCA09, #72A307);
+
+  -webkit-box-shadow: inset 0 2px 2px rgba(255,255,255,0.5), 0 2px 8px rgba(0,0,0,0.2);
+  -moz-box-shadow: inset 0 2px 2px rgba(255,255,255,0.5), 0 2px 8px rgba(0,0,0,0.2);
+  box-shadow: inset 0 2px 2px rgba(255,255,255,0.5), 0 2px 8px rgba(0,0,0,0.2);
+
+  -webkit-border-radius: 10px;
+  -moz-border-radius: 10px;
+  border-radius: 10px;
+
+  border: 1px solid #496805;
+  width: 16px;
+  height: 16px;
+}
+
+.slider > .dragger:hover {
+  background: -webkit-linear-gradient(top, #8DCA09, #8DCA09);
+}
+
+
+.slider > .track, .slider > .highlight-track {
+  background: #ccc;
+  background: -webkit-linear-gradient(top, #bbb, #ddd);
+  background: -moz-linear-gradient(top, #bbb, #ddd);
+  background: linear-gradient(top, #bbb, #ddd);
+
+  -webkit-box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
+  -moz-box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
+  box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
+
+  -webkit-border-radius: 8px;
+  -moz-border-radius: 8px;
+  border-radius: 8px;
+
+  border: 1px solid #aaa;
+  height: 4px;
+}
+
+.slider > .highlight-track {
+	background-color: #8DCA09;
+	background: -webkit-linear-gradient(top, #8DCA09, #72A307);
+	background: -moz-linear-gradient(top, #8DCA09, #72A307);
+	background: linear-gradient(top, #8DCA09, #72A307);
+	
+	border-color: #496805;
+}
+
diff --git a/web/js/simple-slider.coffee b/web/js/simple-slider.coffee
new file mode 100644
index 0000000..9c3d40c
--- /dev/null
+++ b/web/js/simple-slider.coffee
@@ -0,0 +1,361 @@
+###
+ jQuery Simple Slider
+
+ Copyright (c) 2012 James Smith (http://loopj.com)
+
+ Licensed under the MIT license (http://mit-license.org/)
+###
+
+(($, window) ->
+
+  #
+  # Main slider class
+  #
+
+  class SimpleSlider
+    # Build a slider object.
+    # Exposed via el.numericalSlider(options)
+    constructor: (@input, options) ->
+      # Load in the settings
+      @defaultOptions =
+        animate: true
+        snapMid: false
+        classPrefix: null
+        classSuffix: null
+        theme: null
+        highlight: false
+
+      @settings = $.extend({}, @defaultOptions, options)
+      @settings.classSuffix = "-#{@settings.theme}" if @settings.theme
+
+      # Hide the original input
+      @input.hide()
+
+      # Create the slider canvas
+      @slider = $("<div>")
+        .addClass("slider"+(@settings.classSuffix || ""))
+        .css
+          position: "relative"
+          userSelect: "none"
+          boxSizing: "border-box"
+        .insertBefore @input
+      @slider.attr("id", @input.attr("id") + "-slider") if @input.attr("id")
+      
+      @track = @createDivElement("track")
+        .css
+          width: "100%"
+      
+      if @settings.highlight
+        # Create the highlighting track on top of the track
+        @highlightTrack = @createDivElement("highlight-track")
+          .css
+            width: "0"
+      
+      # Create the slider drag target
+      @dragger = @createDivElement("dragger")
+
+      # Adjust dimensions now elements are in the DOM
+      @slider.css
+        minHeight: @dragger.outerHeight()
+        marginLeft: @dragger.outerWidth()/2
+        marginRight: @dragger.outerWidth()/2
+
+      @track.css
+        marginTop: @track.outerHeight()/-2
+  
+      if @settings.highlight
+        @highlightTrack.css
+          marginTop: @track.outerHeight()/-2
+
+      @dragger.css
+        marginTop: @dragger.outerWidth()/-2
+        marginLeft: @dragger.outerWidth()/-2
+
+      # Hook up drag/drop mouse events
+      @track
+        .mousedown (e) =>
+          @trackEvent(e)
+
+      if @settings.highlight
+        @highlightTrack
+          .mousedown (e) =>
+            @trackEvent(e)
+
+      @dragger
+        .mousedown (e) =>
+          return unless e.which == 1
+
+          # We've started moving
+          @dragging = true
+          @dragger.addClass "dragging"
+
+          # Update the slider position
+          @domDrag(e.pageX, e.pageY)
+
+          false
+
+      $("body")
+        .mousemove (e) =>
+          if @dragging
+            # Update the slider position
+            @domDrag(e.pageX, e.pageY)
+
+            # Always show a pointer when dragging
+            $("body").css cursor: "pointer"
+
+
+        .mouseup (e) =>
+          if @dragging
+            # Finished dragging
+            @dragging = false
+            @dragger.removeClass "dragging"
+
+            # Revert the cursor
+            $("body").css cursor: "auto"
+
+      # Set slider initial position
+      @pagePos = 0
+      
+      # Fill in initial slider value
+      if @input.val() == ""
+        @value = @getRange().min
+        @input.val(@value)
+      else
+        @value = @nearestValidValue(@input.val())
+
+      @setSliderPositionFromValue(@value)
+
+      # We are ready to go
+      ratio = @valueToRatio(@value)
+      @input.trigger "slider:ready", 
+        value: @value
+        ratio: ratio
+        position: ratio * @slider.outerWidth()
+        el: @slider
+
+    # Create the basis of the track-div(s)
+    createDivElement: (classname) ->
+      item = $("<div>")
+        .addClass(classname)
+        .css
+          position: "absolute"
+          top: "50%"
+          userSelect: "none"
+          cursor: "pointer"
+        .appendTo @slider
+      return item
+    
+
+    # Set the ratio (value between 0 and 1) of the slider.
+    # Exposed via el.slider("setRatio", ratio)
+    setRatio: (ratio) ->
+      # Range-check the ratio
+      ratio = Math.min(1, ratio)
+      ratio = Math.max(0, ratio)
+
+      # Work out the value
+      value = @ratioToValue(ratio)
+
+      # Update the position of the slider on the screen
+      @setSliderPositionFromValue(value)
+
+      # Trigger value changed events
+      @valueChanged(value, ratio, "setRatio")
+
+    # Set the value of the slider
+    # Exposed via el.slider("setValue", value)
+    setValue: (value) ->
+      # Snap value to nearest step or allowedValue
+      value = @nearestValidValue(value)
+
+      # Work out the ratio
+      ratio = @valueToRatio(value)
+
+      # Update the position of the slider on the screen
+      @setSliderPositionFromValue(value)
+
+      # Trigger value changed events
+      @valueChanged(value, ratio, "setValue")
+
+    # Respond to an event on a track
+    trackEvent: (e) -> 
+      return unless e.which == 1
+
+      @domDrag(e.pageX, e.pageY, true)
+      @dragging = true
+      false
+
+    # Respond to a dom drag event
+    domDrag: (pageX, pageY, animate=false) ->
+      # Normalize position within allowed range
+      pagePos = pageX - @slider.offset().left
+      pagePos = Math.min(@slider.outerWidth(), pagePos)
+      pagePos = Math.max(0, pagePos)
+
+      # If the element position has changed, do stuff
+      if @pagePos != pagePos
+        @pagePos = pagePos
+
+        # Set the percentage value of the slider
+        ratio = pagePos / @slider.outerWidth()
+
+        # Trigger value changed events
+        value = @ratioToValue(ratio)
+        @valueChanged(value, ratio, "domDrag")
+
+        # Update the position of the slider on the screen
+        if @settings.snap
+          @setSliderPositionFromValue(value, animate)
+        else
+          @setSliderPosition(pagePos, animate)
+          
+    # Set the slider position given a slider canvas position
+    setSliderPosition: (position, animate=false) ->
+      if animate and @settings.animate
+        @dragger.animate left: position, 200
+        @highlightTrack.animate width: position, 200 if @settings.highlight
+      else
+        @dragger.css left: position
+        @highlightTrack.css width: position if @settings.highlight
+
+    # Set the slider position given a value
+    setSliderPositionFromValue: (value, animate=false) ->
+      # Get the slide ratio from the value
+      ratio = @valueToRatio(value)
+      
+      # Set the slider position
+      @setSliderPosition(ratio * @slider.outerWidth(), animate)
+
+    # Get the valid range of values
+    getRange: ->
+      if @settings.allowedValues
+        min: Math.min(@settings.allowedValues...)
+        max: Math.max(@settings.allowedValues...)
+      else if @settings.range
+        min: parseFloat(@settings.range[0])
+        max: parseFloat(@settings.range[1])
+      else
+        min: 0
+        max: 1
+
+    # Find the nearest valid value, checking allowedValues and step settings
+    nearestValidValue: (rawValue) ->
+      range = @getRange()
+
+      # Range-check the value
+      rawValue = Math.min(range.max, rawValue)
+      rawValue = Math.max(range.min, rawValue)
+
+      # Apply allowedValues or step settings
+      if @settings.allowedValues
+        closest = null
+        $.each @settings.allowedValues, ->
+          if closest == null || Math.abs(this - rawValue) < Math.abs(closest - rawValue)
+            closest = this
+        
+        return closest
+      else if @settings.step
+        maxSteps = (range.max - range.min) / @settings.step
+        steps = Math.floor((rawValue - range.min) / @settings.step)
+        steps += 1 if (rawValue - range.min) % @settings.step > @settings.step / 2 and steps < maxSteps
+
+        return steps * @settings.step + range.min
+      else
+        return rawValue
+
+    # Convert a value to a ratio
+    valueToRatio: (value) ->
+      if @settings.equalSteps        
+        # Get slider ratio for equal-step
+        for allowedVal, idx in @settings.allowedValues
+          if !closest? || Math.abs(allowedVal - value) < Math.abs(closest - value)
+            closest = allowedVal
+            closestIdx = idx
+
+        if @settings.snapMid
+          (closestIdx+0.5)/@settings.allowedValues.length
+        else
+          (closestIdx)/(@settings.allowedValues.length - 1)
+        
+      else
+        # Get slider ratio for continuous values
+        range = @getRange()
+        (value - range.min) / (range.max - range.min)
+
+    # Convert a ratio to a valid value
+    ratioToValue: (ratio) ->
+      if @settings.equalSteps
+        steps = @settings.allowedValues.length
+        step = Math.round(ratio * steps - 0.5)
+        idx = Math.min(step, @settings.allowedValues.length - 1)
+
+        @settings.allowedValues[idx]
+      else
+        range = @getRange()
+        rawValue = ratio * (range.max - range.min) + range.min
+
+        @nearestValidValue(rawValue)
+
+    # Trigger value changed events
+    valueChanged: (value, ratio, trigger) ->
+      return if value.toString() == @value.toString()
+
+      # Save the new value
+      @value = value
+
+      # Construct event data and fire event
+      eventData = 
+        value: value
+        ratio: ratio
+        position: ratio * @slider.outerWidth()
+        trigger: trigger
+        el: @slider
+
+      @input
+        .val(value)
+        .trigger($.Event("change", eventData))
+        .trigger("slider:changed", eventData)
+
+
+  #
+  # Expose as jQuery Plugin
+  #
+
+  $.extend $.fn, simpleSlider: (settingsOrMethod, params...) ->
+    publicMethods = ["setRatio", "setValue"]
+
+    $(this).each ->
+      if settingsOrMethod and settingsOrMethod in publicMethods
+        obj = $(this).data("slider-object")
+        
+        obj[settingsOrMethod].apply(obj, params)
+      else
+        settings = settingsOrMethod
+        $(this).data "slider-object", new SimpleSlider($(this), settings)
+
+
+  #
+  # Attach unobtrusive JS hooks
+  #
+
+  $ ->
+    $("[data-slider]").each ->
+      $el = $(this)
+
+      # Build options object from data attributes
+      settings = {}
+
+      allowedValues = $el.data "slider-values"
+      settings.allowedValues = (parseFloat(x) for x in allowedValues.split(",")) if allowedValues
+      settings.range = $el.data("slider-range").split(",") if $el.data("slider-range")
+      settings.step = $el.data("slider-step") if $el.data("slider-step")
+      settings.snap = $el.data("slider-snap")
+      settings.equalSteps = $el.data("slider-equal-steps")
+      settings.theme = $el.data("slider-theme") if $el.data("slider-theme")
+      settings.highlight = $el.data("slider-highlight") if $el.attr("data-slider-highlight")
+      settings.animate = $el.data("slider-animate") if $el.data("slider-animate")?
+
+      # Activate the plugin
+      $el.simpleSlider settings
+
+) @jQuery or @Zepto, this
diff --git a/web/js/simple-slider.js b/web/js/simple-slider.js
new file mode 100644
index 0000000..1b917f6
--- /dev/null
+++ b/web/js/simple-slider.js
@@ -0,0 +1,363 @@
+/*
+ jQuery Simple Slider
+
+ Copyright (c) 2012 James Smith (http://loopj.com)
+
+ Licensed under the MIT license (http://mit-license.org/)
+*/
+
+var __slice = [].slice,
+  __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
+
+(function($, window) {
+  var SimpleSlider;
+  SimpleSlider = (function() {
+
+    function SimpleSlider(input, options) {
+      var ratio,
+        _this = this;
+      this.input = input;
+      this.defaultOptions = {
+        animate: true,
+        snapMid: false,
+        classPrefix: null,
+        classSuffix: null,
+        theme: null,
+        highlight: false
+      };
+      this.settings = $.extend({}, this.defaultOptions, options);
+      if (this.settings.theme) {
+        this.settings.classSuffix = "-" + this.settings.theme;
+      }
+      this.input.hide();
+      this.slider = $("<div>").addClass("slider" + (this.settings.classSuffix || "")).css({
+        position: "relative",
+        userSelect: "none",
+        boxSizing: "border-box"
+      }).insertBefore(this.input);
+      if (this.input.attr("id")) {
+        this.slider.attr("id", this.input.attr("id") + "-slider");
+      }
+      this.track = this.createDivElement("track").css({
+        width: "100%"
+      });
+      if (this.settings.highlight) {
+        this.highlightTrack = this.createDivElement("highlight-track").css({
+          width: "0"
+        });
+      }
+      this.dragger = this.createDivElement("dragger");
+      this.slider.css({
+        minHeight: this.dragger.outerHeight(),
+        marginLeft: this.dragger.outerWidth() / 2,
+        marginRight: this.dragger.outerWidth() / 2
+      });
+      this.track.css({
+        marginTop: this.track.outerHeight() / -2
+      });
+      if (this.settings.highlight) {
+        this.highlightTrack.css({
+          marginTop: this.track.outerHeight() / -2
+        });
+      }
+      this.dragger.css({
+        marginTop: this.dragger.outerWidth() / -2,
+        marginLeft: this.dragger.outerWidth() / -2
+      });
+      this.track.mousedown(function(e) {
+        return _this.trackEvent(e);
+      });
+      if (this.settings.highlight) {
+        this.highlightTrack.mousedown(function(e) {
+          return _this.trackEvent(e);
+        });
+      }
+      this.dragger.mousedown(function(e) {
+        if (e.which !== 1) {
+          return;
+        }
+        _this.dragging = true;
+        _this.dragger.addClass("dragging");
+        _this.domDrag(e.pageX, e.pageY);
+        return false;
+      });
+      $("body").mousemove(function(e) {
+        if (_this.dragging) {
+          _this.domDrag(e.pageX, e.pageY);
+          return $("body").css({
+            cursor: "pointer"
+          });
+        }
+      }).mouseup(function(e) {
+        if (_this.dragging) {
+          _this.dragging = false;
+          _this.dragger.removeClass("dragging");
+          return $("body").css({
+            cursor: "auto"
+          });
+        }
+      });
+      this.pagePos = 0;
+      if (this.input.val() === "") {
+        this.value = this.getRange().min;
+        this.input.val(this.value);
+      } else {
+        this.value = this.nearestValidValue(this.input.val());
+      }
+      this.setSliderPositionFromValue(this.value);
+      ratio = this.valueToRatio(this.value);
+      this.input.trigger("slider:ready", {
+        value: this.value,
+        ratio: ratio,
+        position: ratio * this.slider.outerWidth(),
+        el: this.slider
+      });
+    }
+
+    SimpleSlider.prototype.createDivElement = function(classname) {
+      var item;
+      item = $("<div>").addClass(classname).css({
+        position: "absolute",
+        top: "50%",
+        userSelect: "none",
+        cursor: "pointer"
+      }).appendTo(this.slider);
+      return item;
+    };
+
+    SimpleSlider.prototype.setRatio = function(ratio) {
+      var value;
+      ratio = Math.min(1, ratio);
+      ratio = Math.max(0, ratio);
+      value = this.ratioToValue(ratio);
+      this.setSliderPositionFromValue(value);
+      return this.valueChanged(value, ratio, "setRatio");
+    };
+
+    SimpleSlider.prototype.setValue = function(value) {
+      var ratio;
+      value = this.nearestValidValue(value);
+      ratio = this.valueToRatio(value);
+      this.setSliderPositionFromValue(value);
+      return this.valueChanged(value, ratio, "setValue");
+    };
+
+    SimpleSlider.prototype.trackEvent = function(e) {
+      if (e.which !== 1) {
+        return;
+      }
+      this.domDrag(e.pageX, e.pageY, true);
+      this.dragging = true;
+      return false;
+    };
+
+    SimpleSlider.prototype.domDrag = function(pageX, pageY, animate) {
+      var pagePos, ratio, value;
+      if (animate == null) {
+        animate = false;
+      }
+      pagePos = pageX - this.slider.offset().left;
+      pagePos = Math.min(this.slider.outerWidth(), pagePos);
+      pagePos = Math.max(0, pagePos);
+      if (this.pagePos !== pagePos) {
+        this.pagePos = pagePos;
+        ratio = pagePos / this.slider.outerWidth();
+        value = this.ratioToValue(ratio);
+        this.valueChanged(value, ratio, "domDrag");
+        if (this.settings.snap) {
+          return this.setSliderPositionFromValue(value, animate);
+        } else {
+          return this.setSliderPosition(pagePos, animate);
+        }
+      }
+    };
+
+    SimpleSlider.prototype.setSliderPosition = function(position, animate) {
+      if (animate == null) {
+        animate = false;
+      }
+      if (animate && this.settings.animate) {
+        this.dragger.animate({
+          left: position
+        }, 200);
+        if (this.settings.highlight) {
+          return this.highlightTrack.animate({
+            width: position
+          }, 200);
+        }
+      } else {
+        this.dragger.css({
+          left: position
+        });
+        if (this.settings.highlight) {
+          return this.highlightTrack.css({
+            width: position
+          });
+        }
+      }
+    };
+
+    SimpleSlider.prototype.setSliderPositionFromValue = function(value, animate) {
+      var ratio;
+      if (animate == null) {
+        animate = false;
+      }
+      ratio = this.valueToRatio(value);
+      return this.setSliderPosition(ratio * this.slider.outerWidth(), animate);
+    };
+
+    SimpleSlider.prototype.getRange = function() {
+      if (this.settings.allowedValues) {
+        return {
+          min: Math.min.apply(Math, this.settings.allowedValues),
+          max: Math.max.apply(Math, this.settings.allowedValues)
+        };
+      } else if (this.settings.range) {
+        return {
+          min: parseFloat(this.settings.range[0]),
+          max: parseFloat(this.settings.range[1])
+        };
+      } else {
+        return {
+          min: 0,
+          max: 1
+        };
+      }
+    };
+
+    SimpleSlider.prototype.nearestValidValue = function(rawValue) {
+      var closest, maxSteps, range, steps;
+      range = this.getRange();
+      rawValue = Math.min(range.max, rawValue);
+      rawValue = Math.max(range.min, rawValue);
+      if (this.settings.allowedValues) {
+        closest = null;
+        $.each(this.settings.allowedValues, function() {
+          if (closest === null || Math.abs(this - rawValue) < Math.abs(closest - rawValue)) {
+            return closest = this;
+          }
+        });
+        return closest;
+      } else if (this.settings.step) {
+        maxSteps = (range.max - range.min) / this.settings.step;
+        steps = Math.floor((rawValue - range.min) / this.settings.step);
+        if ((rawValue - range.min) % this.settings.step > this.settings.step / 2 && steps < maxSteps) {
+          steps += 1;
+        }
+        return steps * this.settings.step + range.min;
+      } else {
+        return rawValue;
+      }
+    };
+
+    SimpleSlider.prototype.valueToRatio = function(value) {
+      var allowedVal, closest, closestIdx, idx, range, _i, _len, _ref;
+      if (this.settings.equalSteps) {
+        _ref = this.settings.allowedValues;
+        for (idx = _i = 0, _len = _ref.length; _i < _len; idx = ++_i) {
+          allowedVal = _ref[idx];
+          if (!(typeof closest !== "undefined" && closest !== null) || Math.abs(allowedVal - value) < Math.abs(closest - value)) {
+            closest = allowedVal;
+            closestIdx = idx;
+          }
+        }
+        if (this.settings.snapMid) {
+          return (closestIdx + 0.5) / this.settings.allowedValues.length;
+        } else {
+          return closestIdx / (this.settings.allowedValues.length - 1);
+        }
+      } else {
+        range = this.getRange();
+        return (value - range.min) / (range.max - range.min);
+      }
+    };
+
+    SimpleSlider.prototype.ratioToValue = function(ratio) {
+      var idx, range, rawValue, step, steps;
+      if (this.settings.equalSteps) {
+        steps = this.settings.allowedValues.length;
+        step = Math.round(ratio * steps - 0.5);
+        idx = Math.min(step, this.settings.allowedValues.length - 1);
+        return this.settings.allowedValues[idx];
+      } else {
+        range = this.getRange();
+        rawValue = ratio * (range.max - range.min) + range.min;
+        return this.nearestValidValue(rawValue);
+      }
+    };
+
+    SimpleSlider.prototype.valueChanged = function(value, ratio, trigger) {
+      var eventData;
+      if (value.toString() === this.value.toString()) {
+        return;
+      }
+      this.value = value;
+      eventData = {
+        value: value,
+        ratio: ratio,
+        position: ratio * this.slider.outerWidth(),
+        trigger: trigger,
+        el: this.slider
+      };
+      return this.input.val(value).trigger($.Event("change", eventData)).trigger("slider:changed", eventData);
+    };
+
+    return SimpleSlider;
+
+  })();
+  $.extend($.fn, {
+    simpleSlider: function() {
+      var params, publicMethods, settingsOrMethod;
+      settingsOrMethod = arguments[0], params = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
+      publicMethods = ["setRatio", "setValue"];
+      return $(this).each(function() {
+        var obj, settings;
+        if (settingsOrMethod && __indexOf.call(publicMethods, settingsOrMethod) >= 0) {
+          obj = $(this).data("slider-object");
+          return obj[settingsOrMethod].apply(obj, params);
+        } else {
+          settings = settingsOrMethod;
+          return $(this).data("slider-object", new SimpleSlider($(this), settings));
+        }
+      });
+    }
+  });
+  return $(function() {
+    return $("[data-slider]").each(function() {
+      var $el, allowedValues, settings, x;
+      $el = $(this);
+      settings = {};
+      allowedValues = $el.data("slider-values");
+      if (allowedValues) {
+        settings.allowedValues = (function() {
+          var _i, _len, _ref, _results;
+          _ref = allowedValues.split(",");
+          _results = [];
+          for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+            x = _ref[_i];
+            _results.push(parseFloat(x));
+          }
+          return _results;
+        })();
+      }
+      if ($el.data("slider-range")) {
+        settings.range = $el.data("slider-range").split(",");
+      }
+      if ($el.data("slider-step")) {
+        settings.step = $el.data("slider-step");
+      }
+      settings.snap = $el.data("slider-snap");
+      settings.equalSteps = $el.data("slider-equal-steps");
+      if ($el.data("slider-theme")) {
+        settings.theme = $el.data("slider-theme");
+      }
+      if ($el.attr("data-slider-highlight")) {
+        settings.highlight = $el.data("slider-highlight");
+      }
+      if ($el.data("slider-animate") != null) {
+        settings.animate = $el.data("slider-animate");
+      }
+      return $el.simpleSlider(settings);
+    });
+  });
+})(this.jQuery || this.Zepto, this);
diff --git a/web/js/simple-slider.min.js b/web/js/simple-slider.min.js
new file mode 100644
index 0000000..b6a7341
--- /dev/null
+++ b/web/js/simple-slider.min.js
@@ -0,0 +1,11 @@
+/*
+ * jQuery Simple Slider: Unobtrusive Numerical Slider
+ * Version 1.0.0
+ *
+ * Copyright (c) 2013 James Smith (http://loopj.com)
+ *
+ * Licensed under the MIT license (http://mit-license.org/)
+ *
+ */
+
+var __slice=[].slice,__indexOf=[].indexOf||function(e){for(var t=0,n=this.length;t<n;t++)if(t in this&&this[t]===e)return t;return-1};(function(e,t){var n;return n=function(){function t(t,n){var r,i=this;this.input=t,this.defaultOptions={animate:!0,snapMid:!1,classPrefix:null,classSuffix:null,theme:null,highlight:!1},this.settings=e.extend({},this.defaultOptions,n),this.settings.theme&&(this.settings.classSuffix="-"+this.settings.theme),this.input.hide(),this.slider=e("<div>").addClass("slider"+(this.settings.classSuffix||"")).css({position:"relative",userSelect:"none",boxSizing:"border-box"}).insertBefore(this.input),this.input.attr("id")&&this.slider.attr("id",this.input.attr("id")+"-slider"),this.track=this.createDivElement("track").css({width:"100%"}),this.settings.highlight&&(this.highlightTrack=this.createDivElement("highlight-track").css({width:"0"})),this.dragger=this.createDivElement("dragger"),this.slider.css({minHeight:this.dragger.outerHeight(),marginLeft:this.dragger.outerWidth()/2,marginRight:this.dragger.outerWidth()/2}),this.track.css({marginTop:this.track.outerHeight()/-2}),this.settings.highlight&&this.highlightTrack.css({marginTop:this.track.outerHeight()/-2}),this.dragger.css({marginTop:this.dragger.outerWidth()/-2,marginLeft:this.dragger.outerWidth()/-2}),this.track.mousedown(function(e){return i.trackEvent(e)}),this.settings.highlight&&this.highlightTrack.mousedown(function(e){return i.trackEvent(e)}),this.dragger.mousedown(function(e){if(e.which!==1)return;return i.dragging=!0,i.dragger.addClass("dragging"),i.domDrag(e.pageX,e.pageY),!1}),e("body").mousemove(function(t){if(i.dragging)return i.domDrag(t.pageX,t.pageY),e("body").css({cursor:"pointer"})}).mouseup(function(t){if(i.dragging)return i.dragging=!1,i.dragger.removeClass("dragging"),e("body").css({cursor:"auto"})}),this.pagePos=0,this.input.val()===""?(this.value=this.getRange().min,this.input.val(this.value)):this.value=this.nearestValidValue(this.input.val()),this.setSliderPositionFromValue(this.value),r=this.valueToRatio(this.value),this.input.trigger("slider:ready",{value:this.value,ratio:r,position:r*this.slider.outerWidth(),el:this.slider})}return t.prototype.createDivElement=function(t){var n;return n=e("<div>").addClass(t).css({position:"absolute",top:"50%",userSelect:"none",cursor:"pointer"}).appendTo(this.slider),n},t.prototype.setRatio=function(e){var t;return e=Math.min(1,e),e=Math.max(0,e),t=this.ratioToValue(e),this.setSliderPositionFromValue(t),this.valueChanged(t,e,"setRatio")},t.prototype.setValue=function(e){var t;return e=this.nearestValidValue(e),t=this.valueToRatio(e),this.setSliderPositionFromValue(e),this.valueChanged(e,t,"setValue")},t.prototype.trackEvent=function(e){if(e.which!==1)return;return this.domDrag(e.pageX,e.pageY,!0),this.dragging=!0,!1},t.prototype.domDrag=function(e,t,n){var r,i,s;n==null&&(n=!1),r=e-this.slider.offset().left,r=Math.min(this.slider.outerWidth(),r),r=Math.max(0,r);if(this.pagePos!==r)return this.pagePos=r,i=r/this.slider.outerWidth(),s=this.ratioToValue(i),this.valueChanged(s,i,"domDrag"),this.settings.snap?this.setSliderPositionFromValue(s,n):this.setSliderPosition(r,n)},t.prototype.setSliderPosition=function(e,t){t==null&&(t=!1);if(t&&this.settings.animate){this.dragger.animate({left:e},200);if(this.settings.highlight)return this.highlightTrack.animate({width:e},200)}else{this.dragger.css({left:e});if(this.settings.highlight)return this.highlightTrack.css({width:e})}},t.prototype.setSliderPositionFromValue=function(e,t){var n;return t==null&&(t=!1),n=this.valueToRatio(e),this.setSliderPosition(n*this.slider.outerWidth(),t)},t.prototype.getRange=function(){return this.settings.allowedValues?{min:Math.min.apply(Math,this.settings.allowedValues),max:Math.max.apply(Math,this.settings.allowedValues)}:this.settings.range?{min:parseFloat(this.settings.range[0]),max:parseFloat(this.settings.range[1])}:{min:0,max:1}},t.prototype.nearestValidValue=function(t){var n,r,i,s;return i=this.getRange(),t=Math.min(i.max,t),t=Math.max(i.min,t),this.settings.allowedValues?(n=null,e.each(this.settings.allowedValues,function(){if(n===null||Math.abs(this-t)<Math.abs(n-t))return n=this}),n):this.settings.step?(r=(i.max-i.min)/this.settings.step,s=Math.floor((t-i.min)/this.settings.step),(t-i.min)%this.settings.step>this.settings.step/2&&s<r&&(s+=1),s*this.settings.step+i.min):t},t.prototype.valueToRatio=function(e){var t,n,r,i,s,o,u,a;if(this.settings.equalSteps){a=this.settings.allowedValues;for(i=o=0,u=a.length;o<u;i=++o){t=a[i];if(typeof n=="undefined"||n===null||Math.abs(t-e)<Math.abs(n-e))n=t,r=i}return this.settings.snapMid?(r+.5)/this.settings.allowedValues.length:r/(this.settings.allowedValues.length-1)}return s=this.getRange(),(e-s.min)/(s.max-s.min)},t.prototype.ratioToValue=function(e){var t,n,r,i,s;return this.settings.equalSteps?(s=this.settings.allowedValues.length,i=Math.round(e*s-.5),t=Math.min(i,this.settings.allowedValues.length-1),this.settings.allowedValues[t]):(n=this.getRange(),r=e*(n.max-n.min)+n.min,this.nearestValidValue(r))},t.prototype.valueChanged=function(t,n,r){var i;if(t.toString()===this.value.toString())return;return this.value=t,i={value:t,ratio:n,position:n*this.slider.outerWidth(),trigger:r,el:this.slider},this.input.val(t).trigger(e.Event("change",i)).trigger("slider:changed",i)},t}(),e.extend(e.fn,{simpleSlider:function(){var t,r,i;return i=arguments[0],t=2<=arguments.length?__slice.call(arguments,1):[],r=["setRatio","setValue"],e(this).each(function(){var s,o;return i&&__indexOf.call(r,i)>=0?(s=e(this).data("slider-object"),s[i].apply(s,t)):(o=i,e(this).data("slider-object",new n(e(this),o)))})}}),e(function(){return e("[data-slider]").each(function(){var t,n,r,i;return t=e(this),r={},n=t.data("slider-values"),n&&(r.allowedValues=function(){var e,t,r,s;r=n.split(","),s=[];for(e=0,t=r.length;e<t;e++)i=r[e],s.push(parseFloat(i));return s}()),t.data("slider-range")&&(r.range=t.data("slider-range").split(",")),t.data("slider-step")&&(r.step=t.data("slider-step")),r.snap=t.data("slider-snap"),r.equalSteps=t.data("slider-equal-steps"),t.data("slider-theme")&&(r.theme=t.data("slider-theme")),t.attr("data-slider-highlight")&&(r.highlight=t.data("slider-highlight")),t.data("slider-animate")!=null&&(r.animate=t.data("slider-animate")),t.simpleSlider(r)})})})(this.jQuery||this.Zepto,this);
\ No newline at end of file
diff --git a/web/web.php b/web/web.php
new file mode 100644
index 0000000..296387c
--- /dev/null
+++ b/web/web.php
@@ -0,0 +1,91 @@
+<?php
+ /**
+  *  web.php - Simple Web interface for talking to the coke machine lights
+  *  using PHP because screw typing
+  */
+  $cokeaddress = "http://130.95.13.96/";
+?>
+<!DOCTYPE html>
+<html>
+<head>
+	<title>BlinkenLights</title>
+	<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
+	
+	<script src="js/simple-slider.js"></script>
+	<link href="css/simple-slider.css" rel="stylesheet" type="text/css" />
+	<link href="css/simple-slider-volume.css" rel="stylesheet" type="text/css" /> 
+	
+	<script type="text/javascript">
+		function setLED(x, y, r, g, b)
+		{
+			var led = document.getElementById('led'+x+'x'+y);
+			console.log('led'+x+'x'+y);
+			led.style.backgroundColor = "rgb("+r+", "+g+", "+b+")";
+		}
+	</script>
+	<style type="text/css">
+		html {
+			width: 100%;
+			min-width: 480px;
+			background: #DDDDDD;
+		}
+		body {
+			width: 480px;
+			margin-left: auto;
+			margin-right: auto;
+			background: #AAAAAA;
+		}
+		.led {
+			width: 60px;
+			height: 20px;
+			margin: 0px;
+			padding: 0;
+			background: #000000;
+			display: inline-block;
+		}
+		[class^=slider] { display: inline-block; margin-bottom: 30px; }
+	</style>
+</head>
+<body>
+	<div name="lights">
+		<?php
+		
+		for ($i = 0; $i < 7; $i++) {
+			for ($j = 0; $j < 6; $j++) {
+				echo "<div class='led' id='led".$j."x".$i."'>&nbsp;</div>";
+			}
+			echo "<br />";
+		}
+		?>
+	</div>
+	<div class="brightness">
+		Brightness:<br />
+		<input type="text" id="bright" data-slider="true" data-slider-theme="volume" data-slider-range="0,255" data-slider-step="1" />
+	</div>
+	<script>
+	$("[data-slider]")
+	.each(function () {
+		var input = $(this);
+		$("<span>")
+		.addClass("output")
+		.insertAfter($(this));
+	})
+	.bind("slider:ready slider:changed", function (event, data) {
+	  $(this)
+		.nextAll(".output:first")
+		.html(data.value.toFixed(0));
+	});
+	
+	$(".brightness").change(function() {
+		console.log('<?=$cokeaddress?>/brightness?bright='+document.getElementById('bright').value);
+		$.ajax({
+			url: '<?=$cokeaddress?>brightness?bright='+document.getElementById('bright').value,
+			dataType: "jsonp",
+			crossDomain: true
+		});
+		return false;
+	});
+		
+	</script>
+</body>
+</html>
\ No newline at end of file
-- 
GitLab