Working on controller web interface
authorJoseph Coffland <joseph@cauldrondevelopment.com>
Wed, 16 Mar 2016 13:04:39 +0000 (06:04 -0700)
committerJoseph Coffland <joseph@cauldrondevelopment.com>
Wed, 16 Mar 2016 13:04:39 +0000 (06:04 -0700)
32 files changed:
.gitignore
Makefile
bbctrl.py
inevent/EventHandler.py
inevent/JogHandler.py
src/jade/index.jade
src/jade/templates/admin-view.jade [new file with mode: 0644]
src/jade/templates/axis-view.jade [new file with mode: 0644]
src/jade/templates/config-view.jade [new file with mode: 0644]
src/jade/templates/gcode-view.jade [new file with mode: 0644]
src/jade/templates/motor-view.jade [new file with mode: 0644]
src/jade/templates/spindle-view.jade [new file with mode: 0644]
src/jade/templates/status-view.jade [new file with mode: 0644]
src/jade/templates/switches-view.jade [new file with mode: 0644]
src/jade/templates/templated-input.jade [new file with mode: 0644]
src/js/admin-view.js [new file with mode: 0644]
src/js/app.js
src/js/axis-view.js [new file with mode: 0644]
src/js/config-view.js [new file with mode: 0644]
src/js/gcode-view.js [new file with mode: 0644]
src/js/main.js
src/js/motor-view.js [new file with mode: 0644]
src/js/spindle-view.js [new file with mode: 0644]
src/js/status-view.js [new file with mode: 0644]
src/js/switches-view.js [new file with mode: 0644]
src/js/templated-input.js [new file with mode: 0644]
src/resources/config-template.json [new file with mode: 0644]
src/resources/css/side-menu-old-ie.css [new file with mode: 0644]
src/resources/css/side-menu.css [new file with mode: 0644]
src/resources/images/buildbotics_logo.png [new file with mode: 0644]
src/resources/js/ui.js [new file with mode: 0644]
src/stylus/style.styl

index 6f2b850c5a6d7c2bd78d3d9327f73bebfa01595c..70a8c445ad8d7351dfaea5a92ef49a54eb323f53 100644 (file)
@@ -1,6 +1,7 @@
 .sconf_temp/
 .sconsign.dblite
 /http
+/build
 node_modules
 *~
 \#*
index ff09db98012986c690a1aa5bd534584fb08ed452..9e79f2394ec12af99350aaa43653fe5b34ef4297 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -6,48 +6,50 @@ STYLUS     := $(NODE_MODS)/stylus/bin/stylus
 AP         := $(NODE_MODS)/autoprefixer/autoprefixer
 BROWSERIFY := $(NODE_MODS)/browserify/bin/cmd.js
 
-HTTP      := http
 HTML      := index
-HTML      := $(patsubst %,$(HTTP)/%.html,$(HTML))
-CSS       := style
-CSS       := $(patsubst %,$(HTTP)/%.css,$(CSS))
+HTML      := $(patsubst %,http/%.html,$(HTML))
+CSS       := $(wildcard src/stylus/*.styl)
+CSS_ASSETS := build/css/style.css
 JS        := $(wildcard src/js/*.js)
-JS_ASSETS := $(HTTP)/js/assets.js
-TEMPLS    := $(wildcard src/jade/templates/*.jade)
+JS_ASSETS := http/js/assets.js
 STATIC    := $(shell find src/resources -type f)
 STATIC    := $(patsubst src/resources/%,http/%,$(STATIC))
+TEMPLS    := $(wildcard src/jade/templates/*.jade)
 
 ifndef DEST
 DEST=bbctrl/
 endif
 
-WATCH := src/jade src/stylus src/js Makefile
-
-TARGETS := $(HTML) $(CSS) $(JS_ASSETS) $(STATIC)
+WATCH := src/jade src/jade/templates src/stylus src/js src/resources Makefile
 
-all: node_modules $(TARGETS)
+all: html css js static
 
-copy: $(TARGETS)
+copy: all
        cp -r *.py inevent http/ $(DEST)
 
-$(HTTP)/admin.html: build/templates.jade
+html: templates $(HTML)
 
-$(HTTP)/%.html: src/jade/%.jade
-       $(JADE) -P $< --out $(shell dirname $@) || \
-       (rm -f $@; exit 1)
+css: $(CSS_ASSETS) $(CSS_ASSETS).sha256
+       install -D $< http/css/style-$(shell cat $(CSS_ASSETS).sha256).css
 
-$(HTTP)/%.css: src/stylus/%.styl
-       mkdir -p $(shell dirname $@)
-       $(STYLUS) < $< > $@ || (rm -f $@; exit 1)
+js: $(JS_ASSETS) $(JS_ASSETS).sha256
+       install -D $< http/js/assets-$(shell cat $(JS_ASSETS).sha256).js
 
-$(HTTP)/%: src/resources/%
-       install -D $< $@
+static: $(STATIC)
+
+templates: build/templates.jade
 
 build/templates.jade: $(TEMPLS)
        mkdir -p build
        cat $(TEMPLS) >$@
 
-$(JS_ASSETS): $(JS)
+build/hashes.jade: $(CSS_ASSETS).sha256 $(JS_ASSETS).sha256
+       echo "- var css_hash = '$(shell cat $(CSS_ASSETS).sha256)'" > $@
+       echo "- var js_hash = '$(shell cat $(JS_ASSETS).sha256)'" >> $@
+
+http/index.html: build/templates.jade build/hashes.jade
+
+$(JS_ASSETS): $(JS) node_modules
        @mkdir -p $(shell dirname $@)
        $(BROWSERIFY) src/js/main.js -s main -o $@ || \
        (rm -f $@; exit 1)
@@ -55,6 +57,21 @@ $(JS_ASSETS): $(JS)
 node_modules:
        npm install
 
+%.sha256: %
+       mkdir -p $(shell dirname $@)
+       sha256sum $< | sed 's/^\([a-f0-9]\+\) .*$$/\1/' > $@
+
+http/%: src/resources/%
+       install -D $< $@
+
+http/%.html: src/jade/%.jade $(wildcard src/jade/*.jade) node_modules
+       @mkdir -p $(shell dirname $@)
+       $(JADE) -P $< -o http || (rm -f $@; exit 1)
+
+build/css/%.css: src/stylus/%.styl node_modules
+       mkdir -p $(shell dirname $@)
+       $(STYLUS) < $< > $@ || (rm -f $@; exit 1)
+
 watch:
        @clear
        $(MAKE)
@@ -65,5 +82,13 @@ watch:
          $(MAKE); \
        done
 
-clean:
-       rm -rf $(HTTP)
+tidy:
+       rm -f $(shell find "$(DIR)" -name \*~)
+
+clean: tidy
+       rm -rf build html
+
+dist-clean: clean
+       rm -rf node_modules
+
+.PHONY: all install html css static templates clean tidy
index 623f077e262d389f742d4ace14f904130e59b867..daa34575d3a9723bd2bcd4e710e15d2fef26810f 100755 (executable)
--- a/bbctrl.py
+++ b/bbctrl.py
@@ -3,6 +3,8 @@
 ## Change this to match your local settings
 SERIAL_PORT = '/dev/ttyAMA0'
 SERIAL_BAUDRATE = 115200
+HTTP_PORT = 8080
+HTTP_ADDR = '0.0.0.0'
 
 import os
 from tornado import web, ioloop
@@ -45,7 +47,7 @@ class SerialProcess(multiprocessing.Process):
 
     def close(self):
         self.sp.close()
+
 
     def writeSerial(self, data):
         self.sp.write(data.encode())
@@ -57,12 +59,12 @@ class SerialProcess(multiprocessing.Process):
 
     def run(self):
         self.sp.flushInput()
+
         while True:
             # look for incoming tornado request
             if not self.input_queue.empty():
                 data = self.input_queue.get()
+
                 # send it to the serial device
                 self.writeSerial(data)
 
@@ -71,14 +73,17 @@ class SerialProcess(multiprocessing.Process):
                 data = self.readSerial()
                 # send it back to tornado
                 self.output_queue.put(data)
-                print(data.decode('utf-8'))
+                try:
+                    print(data.decode('utf-8'))
+                except Exception as e:
+                    print(e, data)
 
 
 
 class Connection(SockJSConnection):
     def on_open(self, info):
         clients.append(self)
-        self.send(state)
+        self.send(str.encode(json.dumps(state)))
 
 
     def on_close(self):
@@ -114,7 +119,17 @@ class JogHandler(inevent.JogHandler):
     def __init__(self, config):
         super().__init__(config)
 
-        self.lastV = [0.0] * 4
+        self.v = [0.0] * 4
+        self.lastV = self.v
+
+
+    def processed_events(self):
+        if self.v != self.lastV:
+            self.lastV = self.v
+
+            v = ["{:6.5f}".format(x) for x in self.v]
+            cmd = '$jog ' + ' '.join(v) + '\n'
+            input_queue.put(cmd)
 
 
     def changed(self):
@@ -123,17 +138,14 @@ class JogHandler(inevent.JogHandler):
         if self.speed == 3: scale = 1.0 / 4.0
         if self.speed == 4: scale = 1.0
 
-        v = [x * scale for x in self.axes]
+        self.v = [x * scale for x in self.axes]
 
-        if v != self.lastV:
-            self.lastV = v
 
-            v = ["{:6.5f}".format(x) for x in v]
-            cmd = '$jog ' + ' '.join(v) + '\n'
-            input_queue.put(cmd)
 
+def checkEvents():
+    eventProcessor.process_events(eventHandler)
+    eventHandler.processed_events()
 
-def checkEvents(): eventProcessor.process_events(eventHandler)
 eventProcessor = inevent.InEvent(types = "js kbd".split())
 eventHandler = JogHandler(config)
 
@@ -144,9 +156,12 @@ if __name__ == "__main__":
     logging.getLogger().setLevel(logging.DEBUG)
 
     # Start the serial worker
-    sp = SerialProcess(input_queue, output_queue)
-    sp.daemon = True
-    sp.start()
+    try:
+        sp = SerialProcess(input_queue, output_queue)
+        sp.daemon = True
+        sp.start()
+    except Exception as e:
+        print(e)
 
     # Adjust the interval according to frames sent by serial port
     ioloop.PeriodicCallback(checkQueue, 100).start()
@@ -154,5 +169,6 @@ if __name__ == "__main__":
 
     # Start the web server
     app = web.Application(router.urls + handlers)
-    app.listen(8080)
+    app.listen(HTTP_PORT, address = HTTP_ADDR)
+    print('Listening on http://{}:{}/'.format(HTTP_ADDR, HTTP_PORT))
     ioloop.IOLoop.instance().start()
index 7a93a35cc0f103f890e17e1d0429875f8ce6b9dd..97642503a559395b791df8c2743bfe1f5bd8fd2b 100644 (file)
@@ -117,4 +117,3 @@ class EventHandler(object):
       if self.buttons[k] != 0: k_list.append(k)
 
     return k_list
-
index 5ab3fb6934849c60218c646e749c143503096b25..f14b55d58fd6f75d93613ef0b245bbcc963832aa 100644 (file)
@@ -26,7 +26,7 @@ class JogHandler:
     def __init__(self, config):
         self.config = config
         self.axes = [0.0, 0.0, 0.0, 0.0]
-        self.speed = 1
+        self.speed = 3
         self.activate = 0
 
 
@@ -74,7 +74,8 @@ class JogHandler:
             self.axes[axis] = event.stream.state.abs[self.config['axes'][axis]]
             if abs(self.axes[axis]) < self.config['deadband']:
                 self.axes[axis] = 0
-            if not (1 << axis) & self.activate: self.axes[axis] = 0
+            if not (1 << axis) & self.activate and self.activate:
+                self.axes[axis] = 0
 
         if old_axes != self.axes: changed = True
 
index 1118b187ddb2ec91f39a626cdc6c1cb5c829f17b..6431dd6f8cc886078278f6f545c88274c9aaf854 100644 (file)
@@ -1,65 +1,90 @@
+include ../../build/hashes.jade
+
+
 doctype html
 html(lang="en")
   head
-    title Buildbotics Controller - Web interface
     meta(charset="utf-8")
+    meta(name="viewport", content="width=device-width, initial-scale=1.0")
 
-    script(src="//code.jquery.com/jquery-1.11.3.min.js")
-    script(src="//cdn.jsdelivr.net/vue/1.0.13/vue.min.js")
-    script(src="js/sockjs.min.js")
-    script(src="js/gauge.min.js")
-    script(src="js/fd-slider.min.js")
-    script(src="js/assets.js")
+    title Buildbotics Controller - Web interface
+
+    link(rel="stylesheet",
+      href="http://yui.yahooapis.com/pure/0.6.0/pure-min.css")
+    //if lte IE 8
+      link(rel="stylesheet", href="css/side-menu-old-ie.css")
+    // [if gt IE 8] <!
+    link(rel="stylesheet", href="css/side-menu.css")
+    // <![endif]
 
     - var faURL = "//maxcdn.bootstrapcdn.com/font-awesome"
     link(rel="stylesheet" href=faURL + "/4.5.0/css/font-awesome.min.css")
+    link(href='//fonts.googleapis.com/css?family=Audiowide' rel='stylesheet'
+      type='text/css')
+    link(rel='stylesheet' href='//yui.yahooapis.com/pure/0.6.0/pure-min.css')
+
+    link(rel='stylesheet' href='/css/style-' + css_hash + '.css')
 
-    link(rel="stylesheet" href="/style.css")
 
   body
-    table
-      tr
-        td
-        td: button(@click="jog('y', 1)") Y+
-        td
-        td: button(@click="jog('z', 1)") Z+
-        td: button(@click="jog('a', 1)") A+
-      tr
-        td: button(@click="jog('x', -1)") -X
-        td
-        td: button(@click="jog('x', 1)") X+
-      tr
-        td
-        td: button(@click="jog('y', -1)") -Y
-        td
-        td: button(@click="jog('z', -1)") Z-
-        td: button(@click="jog('a', -1)") A-
-
-    h2 Velocity {{state.vel}}
-    h2 Step {{step}}
-
-    table.axes
-      tr
-        th Axis
-        th Position
-        th Step
-        th Flags
-        th Current
-        th StallGuard
-
-      each axis in ['x', 'y', 'z']
-        tr.axis
-          th.name #{axis}
-          td {{state.pos#{axis}}}
-          td {{state.dstep#{axis}}}
-          td {{state.dflags#{axis}}}
-          td
-            | {{state.dcur#{axis} | percent}}
-            gauge(:value="state.dcur#{axis} * 32", :min="1", :max="32")
-            input(type="range" min=1 max=32 v-model="current#{axis}")
-          td
-            gauge(:value="state.sguard#{axis}", :min="0", :max="511")
+    #layout
+      a#menuLink.menu-link(href="#menu"): span
+
+      #menu
+        button.save(:disabled="!modified", class="pure-button button-success"
+          @click="save") Save
+
+        .pure-menu
+          ul.pure-menu-list
+            li.pure-menu-heading
+              a.pure-menu-link(href="#status") Status
+
+            li.pure-menu-heading
+              a.pure-menu-link(href="#motor:0") Motors
+
+            li.pure-menu-item(v-for="motor in config.motors")
+              a.pure-menu-link(:href="'#motor:' + $index") Motor {{$index}}
 
+            li.pure-menu-heading
+              a.pure-menu-link(href="#axis:x") Axes
+
+            li.pure-menu-item(v-for="axis in config.axes")
+              a.pure-menu-link(:href="'#axis:' + $key")
+                | {{$key | capitalize}} Axis
+
+            li.pure-menu-heading
+              a.pure-menu-link(href="#spindle") Spindle
+
+            li.pure-menu-heading
+              a.pure-menu-link(href="#switches") Switches
+
+            li.pure-menu-heading
+              a.pure-menu-link(href="#gcode") Gcode
+
+            li.pure-menu-heading
+              a.pure-menu-link(href="#admin") Admin
+
+      #main
+        .header
+          img(src="/images/buildbotics_logo.png")
+          .logo
+            span.left Build
+            span.right botics
+          h2 Machine Controller
+
+        .content
+          component(:is="currentView + '-view'", :index="index",
+            :config="config", :template="template")
 
     #templates
+      include ../../build/templates.jade
+
+    script(src="//code.jquery.com/jquery-1.11.3.min.js")
+    script(src="//cdn.jsdelivr.net/vue/1.0.17/vue.js")
+    script(src="js/sockjs.min.js")
+    script(src="js/gauge.min.js")
+    script(src="js/fd-slider.min.js")
+
+    script(src='/js/assets-' + js_hash + '.js')
 
+    script(src="js/ui.js")
diff --git a/src/jade/templates/admin-view.jade b/src/jade/templates/admin-view.jade
new file mode 100644 (file)
index 0000000..46c18f1
--- /dev/null
@@ -0,0 +1,4 @@
+script#admin-view-template(type="text/x-template")
+  button.pure-button.pure-button-primary(@click="backup") Backup configuration
+  button.pure-button.pure-button-primary(@click="restore") Restore configuration
+  button.pure-button.pure-button-primary(@click="upgrade") Upgrade firmware
diff --git a/src/jade/templates/axis-view.jade b/src/jade/templates/axis-view.jade
new file mode 100644 (file)
index 0000000..7b2fff8
--- /dev/null
@@ -0,0 +1,10 @@
+script#axis-view-template(type="text/x-template")
+  #axis
+    h1 {{index | capitalize}} Axis Configuration
+
+    form.pure-form.pure-form-aligned
+      fieldset(v-for="category in template.axes", :class="$key")
+        h2 {{$key}}
+
+        templated-input(v-for="templ in category", :name="$key",
+          :model.sync="axis[$key]", :template="templ")
diff --git a/src/jade/templates/config-view.jade b/src/jade/templates/config-view.jade
new file mode 100644 (file)
index 0000000..a1747df
--- /dev/null
@@ -0,0 +1,14 @@
+script#config-view-template(type="text/x-template")
+  #config-page
+    h1.title {{page}} {{motor}}
+
+    .buttons
+      button(@click="back") Back
+      button(@click="next") Next
+
+    component(:is="page + '-view'", :config="config.motors[motor]",
+      :template="template")
+
+    .buttons
+      button(@click="back") Back
+      button(@click="next") Next
diff --git a/src/jade/templates/gcode-view.jade b/src/jade/templates/gcode-view.jade
new file mode 100644 (file)
index 0000000..7941f2d
--- /dev/null
@@ -0,0 +1,8 @@
+script#gcode-view-template(type="text/x-template")
+  #gcode
+    h1 GCode Configuration
+
+    form.pure-form.pure-form-aligned
+      fieldset
+        templated-input(v-for="templ in template.gcode", :name="$key",
+          :model.sync="gcode[$key]", :template="templ")
diff --git a/src/jade/templates/motor-view.jade b/src/jade/templates/motor-view.jade
new file mode 100644 (file)
index 0000000..9827706
--- /dev/null
@@ -0,0 +1,10 @@
+script#motor-view-template(type="text/x-template")
+  #motor
+    h1 Motor {{index}} Configuration
+
+    form.pure-form.pure-form-aligned
+      fieldset(v-for="category in template.motors", :class="$key")
+        h2 {{$key}}
+
+        templated-input(v-for="templ in category", :name="$key",
+          :model.sync="motor[$key]", :template="templ")
diff --git a/src/jade/templates/spindle-view.jade b/src/jade/templates/spindle-view.jade
new file mode 100644 (file)
index 0000000..c744d0a
--- /dev/null
@@ -0,0 +1,8 @@
+script#spindle-view-template(type="text/x-template")
+  #spindle
+    h1 Spindle Configuration
+
+    form.pure-form.pure-form-aligned
+      fieldset
+        templated-input(v-for="templ in template.spindle", :name="$key",
+          :model.sync="spindle[$key]", :template="templ")
diff --git a/src/jade/templates/status-view.jade b/src/jade/templates/status-view.jade
new file mode 100644 (file)
index 0000000..037f5b4
--- /dev/null
@@ -0,0 +1,43 @@
+script#status-view-template(type="text/x-template")
+  table
+    tr
+      td
+      td: button(@click="jog('y', 1)") Y+
+      td
+      td: button(@click="jog('z', 1)") Z+
+      td: button(@click="jog('a', 1)") A+
+    tr
+      td: button(@click="jog('x', -1)") -X
+      td
+      td: button(@click="jog('x', 1)") X+
+    tr
+      td
+      td: button(@click="jog('y', -1)") -Y
+      td
+      td: button(@click="jog('z', -1)") Z-
+      td: button(@click="jog('a', -1)") A-
+
+  h2 Velocity {{state.vel}}
+  h2 Step {{step}}
+
+  table.axes
+    tr
+      th Axis
+      th Position
+      th Step
+      th Flags
+      th Current
+      th StallGuard
+
+    each axis in ['x', 'y', 'z']
+      tr.axis
+        th.name #{axis}
+        td {{state.pos#{axis}}}
+        td {{state.dstep#{axis}}}
+        td {{state.dflags#{axis}}}
+        td
+          | {{state.dcur#{axis} | percent}}
+          gauge(:value="state.dcur#{axis} * 32", :min="1", :max="32")
+          input(type="range" min=1 max=32 v-model="current#{axis}")
+        td
+          gauge(:value="state.sguard#{axis}", :min="0", :max="511")
diff --git a/src/jade/templates/switches-view.jade b/src/jade/templates/switches-view.jade
new file mode 100644 (file)
index 0000000..0769e09
--- /dev/null
@@ -0,0 +1,10 @@
+script#switches-view-template(type="text/x-template")
+  #switches
+    h1 Switch Configuration
+
+    form.pure-form.pure-form-aligned
+      fieldset
+        .switch(v-for="switch in switches")
+          h3 Switch {{$index}}
+          templated-input(v-for="templ in template.switches", :name="$key",
+            :model.sync="switch[$key]", :template="templ")
diff --git a/src/jade/templates/templated-input.jade b/src/jade/templates/templated-input.jade
new file mode 100644 (file)
index 0000000..26200a3
--- /dev/null
@@ -0,0 +1,26 @@
+script#templated-input-template(type="text/x-template")
+  .pure-control-group(:class="name")
+    label(:for="name") {{name}}
+
+    select(v-if="template.type == 'enum'", v-model="model",
+      :id="name", @change="change")
+      option(v-for="opt in template.values", :value="opt") {{opt}}
+
+    input(v-if="template.type == 'bool'", type="checkbox",
+      v-model="model", :id="name", @change="change")
+
+    input(v-if="template.type == 'float'", v-model="model", number,
+      :min="template.min", :max="template.max", step="any", type="number",
+        :id="name", @change="change")
+
+    input(v-if="template.type == 'int'", v-model="model", number,
+      :min="template.min", :max="template.max", type="number",
+        :id="name", @change="change")
+
+    input(v-if="template.type == 'string'", v-model="model",
+      type="text", :id="name", @change="change")
+
+    textarea(v-if="template.type == 'text'", v-model="model",
+      :id="name", @change="change")
+
+    label.units {{template.unit}}
diff --git a/src/js/admin-view.js b/src/js/admin-view.js
new file mode 100644 (file)
index 0000000..96be9eb
--- /dev/null
@@ -0,0 +1,21 @@
+'use strict'
+
+
+module.exports = {
+  template: '#admin-view-template',
+  props: ['config'],
+
+
+  ready: function () {
+  },
+
+
+  methods: {
+    backup: function () {
+    },
+
+
+    restore: function () {
+    }
+  }
+}
index 4ce1b88028b5a237e76ccbefa884838fe77de6b5..1f2438644706a0d130bdb51183e7606c890f960b 100644 (file)
@@ -1,85 +1,65 @@
 'use strict'
 
 
-function is_array(x) {
-  return Object.prototype.toString.call(x) === '[object Array]';
-}
-
-
 module.exports = new Vue({
   el: 'body',
 
 
   data: function () {
     return {
-      axes: 'xyza',
-      state: {
-        dcurx: 1, dcury: 1, dcurz: 1, dcura: 1
-        //sguardx: 1, sguardy: 1, sguardz: 1, sguarda: 1
-      },
-      step: 10
+      currentView: 'loading',
+      index: -1,
+      modified: false,
+      template: {"motors": {}, "axes": {}},
+      config: {"motors": [{}]}
     }
   },
 
 
   components: {
-    gauge: require('./gauge')
+    'loading-view': {template: '<h1>Loading...</h1>'},
+    'status-view': require('./status-view'),
+    'axis-view': require('./axis-view'),
+    'motor-view': require('./motor-view'),
+    'spindle-view': require('./spindle-view'),
+    'switches-view': require('./switches-view'),
+    'gcode-view': require('./gcode-view'),
+    'admin-view': require('./admin-view')
   },
 
 
-  watch: {
-    currentx: function (value) {this.current('x', value);},
-    currenty: function (value) {this.current('y', value);},
-    currentz: function (value) {this.current('z', value);},
-    currenta: function (value) {this.current('a', value);}
+  events: {
+    'config-changed': function () {this.modified = true;}
   },
 
 
   ready: function () {
-    this.sock = new SockJS('//' + window.location.host + '/ws');
-
-    this.sock.onmessage = function (e) {
-      var data = e.data;
-      console.debug(data);
+    $.get('/config-template.json').success(function (data, status, xhr) {
+      this.template = data;
 
-      for (var key in data) {
-        this.$set('state.' + key, data[key]);
+      $.get('/default-config.json').success(function (data, status, xhr) {
+        this.config = data;
 
-        for (var axis of ['x', 'y', 'z', 'a'])
-          if (key == 'dcur' + axis && typeof this.$get('current' + axis) == 'undefined')
-            this.$set('current' + axis, (32 * data[key]).toFixed());
-      }
-    }.bind(this);
+        this.parse_hash();
+        $(window).on('hashchange', this.parse_hash);
+     }.bind(this))
+    }.bind(this))
   },
 
 
   methods: {
-    send: function (data) {
-      this.sock.send(JSON.stringify(data));
-    },
+    parse_hash: function () {
+      var hash = location.hash.substr(1);
+      var parts = hash.split(':');
 
+      if (parts.length == 2) this.index = parts[1];
 
-    jog: function (axis, dir) {
-      var pos = this.state['pos' + axis] + dir * this.step;
-      this.sock.send('g0' + axis + pos);
+      this.currentView = parts[0];
     },
 
 
-    current: function (axis, value) {
-      var x = value / 32.0;
-      if (this.state['dcur' + axis] == x) return;
-
-      var data = {};
-      data['dcur' + axis] = x;
-      this.send(data);
-    }
-  },
-
-
-  filters: {
-    percent: function (value, precision) {
-      if (typeof precision == 'undefined') precision = 2;
-      return (value * 100.0).toFixed(precision) + '%';
+    save: function () {
+      this.modified = false;
     }
   }
 })
diff --git a/src/js/axis-view.js b/src/js/axis-view.js
new file mode 100644 (file)
index 0000000..a50a385
--- /dev/null
@@ -0,0 +1,48 @@
+'use strict'
+
+
+module.exports = {
+  template: '#axis-view-template',
+  props: ['index', 'config', 'template'],
+
+
+  data: function () {
+    return {axis: {}}
+  },
+
+
+  watch: {
+    index: function() {this.update();}
+  },
+
+
+  events: {
+    'input-changed': function() {
+      this.$dispatch('config-changed');
+      return false;
+    }
+  },
+
+
+  ready: function () {
+    this.update();
+  },
+
+
+  methods: {
+    update: function () {
+      Vue.nextTick(function () {
+        if (this.config.hasOwnProperty('axes'))
+          this.axis = this.config.axes[this.index];
+        else this.axes = {};
+
+        var template = this.template.axes;
+        for (var category in template)
+          for (var key in template[category])
+            if (!this.axis.hasOwnProperty(key))
+              this.$set('axis["' + key + '"]',
+                        template[category][key].default);
+      }.bind(this));
+    }
+  }
+}
diff --git a/src/js/config-view.js b/src/js/config-view.js
new file mode 100644 (file)
index 0000000..0a8ea27
--- /dev/null
@@ -0,0 +1,44 @@
+'use strict'
+
+
+module.exports = {
+  template: '#config-view-template',
+
+
+  data: function () {
+    return {
+      page: 'motor',
+      motor: 0,
+      template: {},
+      config: {"motors": [{}]}
+    }
+  },
+
+
+  components: {
+    'motor-view': require('./motor-view'),
+    'switch-view': require('./switch-view')
+  },
+
+
+  ready: function () {
+    $.get('/config-template.json').success(function (data, status, xhr) {
+      this.template = data;
+
+      $.get('/default-config.json').success(function (data, status, xhr) {
+        this.config = data;
+      }.bind(this))
+    }.bind(this))
+  },
+
+
+  methods: {
+    back: function() {
+      if (this.motor) this.motor--;
+    },
+
+    next: function () {
+      if (this.motor < this.config.motors.length - 1) this.motor++;
+    }
+  }
+}
diff --git a/src/js/gcode-view.js b/src/js/gcode-view.js
new file mode 100644 (file)
index 0000000..9e95a46
--- /dev/null
@@ -0,0 +1,43 @@
+'use strict'
+
+
+module.exports = {
+  template: '#gcode-view-template',
+  props: ['config', 'template'],
+
+
+  data: function () {
+    return {
+      gcode: {}
+    }
+  },
+
+
+  events: {
+    'input-changed': function() {
+      this.$dispatch('config-changed');
+      return false;
+    }
+  },
+
+
+  ready: function () {
+    this.update();
+  },
+
+
+  methods: {
+    update: function () {
+      Vue.nextTick(function () {
+        if (this.config.hasOwnProperty('gcode'))
+          this.gcode = this.config.gcode;
+
+        var template = this.template.gcode;
+        for (var key in template)
+          if (!this.gcode.hasOwnProperty(key))
+            this.$set('gcode["' + key + '"]',
+                      template[key].default);
+      }.bind(this));
+    }
+  }
+}
index 8cff07acfd568bb16c07d89e5014e65ff9046224..4e3ba2ac440644ebefcf610cdc20a26f3cb1bbae 100644 (file)
@@ -3,6 +3,9 @@ $(function() {
   Vue.config.debug = true;
   Vue.util.warn = function (msg) {console.debug('[Vue warn]: ' + msg)}
 
+  // Register global components
+  Vue.component('templated-input', require('./templated-input'));
+
   // Vue app
   require('./app');
 });
diff --git a/src/js/motor-view.js b/src/js/motor-view.js
new file mode 100644 (file)
index 0000000..2237ff5
--- /dev/null
@@ -0,0 +1,50 @@
+'use strict'
+
+
+module.exports = {
+  template: '#motor-view-template',
+  props: ['index', 'config', 'template'],
+
+
+  data: function () {
+    return {
+      motor: {}
+    }
+  },
+
+
+  watch: {
+    index: function() {this.update();}
+  },
+
+
+  events: {
+    'input-changed': function() {
+      this.$dispatch('config-changed');
+      return false;
+    }
+  },
+
+
+  ready: function () {
+    this.update();
+  },
+
+
+  methods: {
+    update: function () {
+      Vue.nextTick(function () {
+        if (this.config.hasOwnProperty('motors'))
+          this.motor = this.config.motors[this.index];
+        else this.motor = {};
+
+        var template = this.template.motors;
+        for (var category in template)
+          for (var key in template[category])
+            if (!this.motor.hasOwnProperty(key))
+              this.$set('motor["' + key + '"]',
+                        template[category][key].default);
+      }.bind(this));
+    }
+  }
+}
diff --git a/src/js/spindle-view.js b/src/js/spindle-view.js
new file mode 100644 (file)
index 0000000..2423ff1
--- /dev/null
@@ -0,0 +1,43 @@
+'use strict'
+
+
+module.exports = {
+  template: '#spindle-view-template',
+  props: ['config', 'template'],
+
+
+  data: function () {
+    return {
+      spindle: {}
+    }
+  },
+
+
+  events: {
+    'input-changed': function() {
+      this.$dispatch('config-changed');
+      return false;
+    }
+  },
+
+
+  ready: function () {
+    this.update();
+  },
+
+
+  methods: {
+    update: function () {
+      Vue.nextTick(function () {
+        if (this.config.hasOwnProperty('spindle'))
+          this.spindle = this.config.spindle;
+
+        var template = this.template.spindle;
+        for (var key in template)
+          if (!this.spindle.hasOwnProperty(key))
+            this.$set('spindle["' + key + '"]',
+                      template[key].default);
+      }.bind(this));
+    }
+  }
+}
diff --git a/src/js/status-view.js b/src/js/status-view.js
new file mode 100644 (file)
index 0000000..1cc7bef
--- /dev/null
@@ -0,0 +1,86 @@
+'use strict'
+
+
+function is_array(x) {
+  return Object.prototype.toString.call(x) === '[object Array]';
+}
+
+
+module.exports = {
+  template: '#status-view-template',
+
+
+  data: function () {
+    return {
+      axes: 'xyza',
+      state: {
+        dcurx: 1, dcury: 1, dcurz: 1, dcura: 1
+        //sguardx: 1, sguardy: 1, sguardz: 1, sguarda: 1
+      },
+      step: 10
+    }
+  },
+
+
+  components: {
+    gauge: require('./gauge')
+  },
+
+
+  watch: {
+    currentx: function (value) {this.current('x', value);},
+    currenty: function (value) {this.current('y', value);},
+    currentz: function (value) {this.current('z', value);},
+    currenta: function (value) {this.current('a', value);}
+  },
+
+
+  ready: function () {
+    this.sock = new SockJS('//' + window.location.host + '/ws');
+
+    this.sock.onmessage = function (e) {
+      var data = e.data;
+      console.debug(data);
+
+      for (var key in data) {
+        this.$set('state.' + key, data[key]);
+
+        for (var axis of ['x', 'y', 'z', 'a'])
+          if (key == 'dcur' + axis &&
+              typeof this.$get('current' + axis) == 'undefined')
+            this.$set('current' + axis, (32 * data[key]).toFixed());
+      }
+    }.bind(this);
+  },
+
+
+  methods: {
+    send: function (data) {
+      this.sock.send(JSON.stringify(data));
+    },
+
+
+    jog: function (axis, dir) {
+      var pos = this.state['pos' + axis] + dir * this.step;
+      this.sock.send('g0' + axis + pos);
+    },
+
+
+    current: function (axis, value) {
+      var x = value / 32.0;
+      if (this.state['dcur' + axis] == x) return;
+
+      var data = {};
+      data['dcur' + axis] = x;
+      this.send(data);
+    }
+  },
+
+
+  filters: {
+    percent: function (value, precision) {
+      if (typeof precision == 'undefined') precision = 2;
+      return (value * 100.0).toFixed(precision) + '%';
+    }
+  }
+}
diff --git a/src/js/switches-view.js b/src/js/switches-view.js
new file mode 100644 (file)
index 0000000..1d4c554
--- /dev/null
@@ -0,0 +1,46 @@
+'use strict'
+
+
+module.exports = {
+  template: '#switches-view-template',
+  props: ['config', 'template'],
+
+
+  data: function () {
+    return {
+      'switches': []
+    }
+  },
+
+
+  events: {
+    'input-changed': function() {
+      this.$dispatch('config-changed');
+      return false;
+    }
+  },
+
+
+  ready: function () {
+    this.update();
+  },
+
+
+  methods: {
+    update: function () {
+      Vue.nextTick(function () {
+        if (this.config.hasOwnProperty('switches'))
+          this.switches = this.config.switches;
+        else this.switches = [];
+
+        for (var i = 0; i < this.switches.length; i++) {
+          var template = this.template.switches;
+          for (var key in template)
+            if (!this.switches[i].hasOwnProperty(key))
+              this.$set('switches[' + i + ']["' + key + '"]',
+                        template[key].default);
+        }
+      }.bind(this));
+    }
+  }
+}
diff --git a/src/js/templated-input.js b/src/js/templated-input.js
new file mode 100644 (file)
index 0000000..8454882
--- /dev/null
@@ -0,0 +1,15 @@
+'use strict'
+
+
+module.exports = {
+  replace: true,
+  template: '#templated-input-template',
+  props: ['name', 'model', 'template'],
+
+
+  methods: {
+    change: function () {
+      this.$dispatch('input-changed');
+    }
+  }
+}
diff --git a/src/resources/config-template.json b/src/resources/config-template.json
new file mode 100644 (file)
index 0000000..35f3f00
--- /dev/null
@@ -0,0 +1,207 @@
+{
+  "axes": {
+    "motion": {
+      "mode": {
+        "type": "enum",
+        "values": ["standard", "radius", "disabled"],
+        "default": "disabled"
+      },
+      "max-velocity": {
+        "type": "float",
+        "min": 0,
+        "unit": "mm/min",
+        "default": 16000
+      },
+      "max-feedrate": {
+        "type": "float",
+        "min": 0, "unit":
+        "mm/min",
+        "default": 16000
+      },
+      "max-jerk": {
+        "type": "float",
+        "min": 0,
+        "unit": "km/min^3",
+        "default": 20
+      },
+      "junction-deviation": {
+        "type": "float",
+        "min": 0,
+        "unit": "mm",
+        "default": 0.05
+      }
+    },
+
+    "limits": {
+      "min-soft-limit": {
+        "type": "float",
+        "unit": "mm",
+        "default": 0
+      },
+      "max-soft-limit": {
+        "type": "float",
+        "unit": "mm",
+        "default": 150
+      },
+      "min-switch": {
+        "type": "int",
+        "unit": "id",
+        "min": 0,
+        "max": 8,
+        "default": 0
+      },
+      "max-switch": {
+        "type": "int",
+        "unit": "id",
+        "min": 0,
+        "max": 8,
+        "default": 0
+      }
+    },
+
+    "homing": {
+      "max-homing-jerk": {
+        "type": "float",
+        "min": 0,
+        "unit": "km/min^3",
+        "default": 40
+      },
+      "search-velocity": {
+        "type": "float",
+        "min": 0,
+        "unit": "mm/min",
+        "default": 500
+      },
+      "latch-velocity": {
+        "type": "float",
+        "min": 0,
+        "unit": "mm/min",
+        "default": 100
+      },
+      "latch-backoff": {
+        "type": "float",
+        "min": 0,
+        "unit": "mm",
+        "default": 5
+      },
+      "zero-backoff": {
+        "type": "float",
+        "min": 0,
+        "unit": "mm",
+        "default": 1
+      }
+    }
+  },
+
+  "motors": {
+    "general": {
+      "axis-mapping": {
+        "type": "enum",
+        "values": ["x", "y", "z", "a", "b", "c"],
+        "default": "x"
+      },
+      "step-angle": {
+        "type": "float",
+        "unit": "degrees",
+        "default": 1.8
+      },
+      "travel-per-rev": {
+        "type": "float",
+        "unit": "mm",
+        "default": 3.175
+      },
+      "microsteps": {
+        "type": "int",
+        "values": [1, 2, 4, 8, 16, 32, 64, 128, 256],
+        "unit": "per full step",
+        "default": 16
+      },
+      "polarity": {
+        "type": "enum",
+        "values": ["normal", "reversed"],
+        "default": "normal"
+      }
+    },
+
+    "power": {
+      "power-mode": {
+        "type": "enum",
+        "values": ["always-on", "in-cycle", "when-moving"],
+        "default": "in-cycle"
+      },
+      "power-level": {
+        "type": "float",
+        "min": 0,
+        "max": 100,
+        "unit": "%",
+        "default": 80
+      },
+      "stall-guard": {
+        "type": "float",
+        "min": 0,
+        "max": 100,
+        "unit": "%",
+        "default": 70
+      },
+      "cool-step": {
+        "type": "bool",
+        "default": true
+      }
+    }
+  },
+
+  "spindle": {
+    "max-speed": {
+      "type": "float",
+      "unit": "RPM",
+      "min": 0,
+      "default": 10000
+    },
+    "type": {
+      "type": "enum",
+      "values": ["RS485", "PWM"],
+      "default": "RS485"
+    },
+    "min-pulse-width": {
+      "type": "float",
+      "unit": "ms",
+      "default": 20
+    },
+    "max-pulse-width": {
+      "type": "float",
+      "unit": "ms",
+      "default": 100
+    },
+    "polarity": {
+      "type": "enum",
+      "values": ["normal", "reversed"],
+      "default": "normal"
+    },
+    "ramp-up-velocity": {
+      "type": "float",
+      "unit": "rev/min^2",
+      "min": 0,
+      "default": 48000
+    },
+    "ramp-down-velocity": {
+      "type": "float",
+      "unit": "rev/min^2",
+      "min": 0,
+      "default": 48000
+    }
+  },
+
+  "switches": {
+    "type": {
+      "type": "enum",
+      "values": ["normally-open", "normally-closed"],
+      "default": "normally-closed"
+    }
+  },
+
+  "gcode": {
+    "preamble": {"type": "text"},
+    "tool-change": {"type": "text"},
+    "epilogue": {"type": "text"}
+  }
+}
diff --git a/src/resources/css/side-menu-old-ie.css b/src/resources/css/side-menu-old-ie.css
new file mode 100644 (file)
index 0000000..0b0e738
--- /dev/null
@@ -0,0 +1,254 @@
+body {
+    color: #777;
+}
+
+.pure-img-responsive {
+    max-width: 100%;
+    height: auto;
+}
+
+/*
+Add transition to containers so they can push in and out.
+*/
+
+#layout,
+#menu,
+.menu-link {
+    -webkit-transition: all 0.2s ease-out;
+    -moz-transition: all 0.2s ease-out;
+    -ms-transition: all 0.2s ease-out;
+    -o-transition: all 0.2s ease-out;
+    transition: all 0.2s ease-out;
+}
+
+/*
+This is the parent `<div>` that contains the menu and the content area.
+*/
+
+#layout {
+    position: relative;
+    padding-left: 0;
+}
+
+#layout.active #menu {
+    left: 150px;
+    width: 150px;
+}
+
+#layout.active .menu-link {
+    left: 150px;
+}
+
+/*
+The content `<div>` is where all your content goes.
+*/
+
+.content {
+    margin: 0 auto;
+    padding: 0 2em;
+    max-width: 800px;
+    margin-bottom: 50px;
+    line-height: 1.6em;
+}
+
+.header {
+    margin: 0;
+    color: #333;
+    text-align: center;
+    padding: 2.5em 2em 0;
+    border-bottom: 1px solid #eee;
+}
+
+.header h1 {
+    margin: 0.2em 0;
+    font-size: 3em;
+    font-weight: 300;
+}
+
+.header h2 {
+    font-weight: 300;
+    color: #ccc;
+    padding: 0;
+    margin-top: 0;
+}
+
+.content-subhead {
+    margin: 50px 0 20px 0;
+    font-weight: 300;
+    color: #888;
+}
+
+/*
+The `#menu` `<div>` is the parent `<div>` that contains the `.pure-menu` that
+appears on the left side of the page.
+*/
+
+#menu {
+    margin-left: -150px;
+    /* "#menu" width */
+    width: 150px;
+    position: fixed;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    z-index: 1000;
+    /* so the menu or its navicon stays above all content */
+    background: #191818;
+    overflow-y: auto;
+    -webkit-overflow-scrolling: touch;
+}
+
+/*
+    All anchors inside the menu should be styled like this.
+    */
+
+#menu a {
+    color: #999;
+    border: none;
+    padding: 0.6em 0 0.6em 0.6em;
+}
+
+/*
+    Remove all background/borders, since we are applying them to #menu.
+    */
+
+#menu .pure-menu,
+#menu .pure-menu ul {
+    border: none;
+    background: transparent;
+}
+
+/*
+    Add that light border to separate items into groups.
+    */
+
+#menu .pure-menu ul,
+#menu .pure-menu .menu-item-divided {
+    border-top: 1px solid #333;
+}
+
+/*
+        Change color of the anchor links on hover/focus.
+        */
+
+#menu .pure-menu li a:hover,
+#menu .pure-menu li a:focus {
+    background: #333;
+}
+
+/*
+    This styles the selected menu item `<li>`.
+    */
+
+#menu .pure-menu-selected,
+#menu .pure-menu-heading {
+    background: #1f8dd6;
+}
+
+/*
+        This styles a link within a selected menu item `<li>`.
+        */
+
+#menu .pure-menu-selected a {
+    color: #fff;
+}
+
+/*
+    This styles the menu heading.
+    */
+
+#menu .pure-menu-heading {
+    font-size: 110%;
+    color: #fff;
+    margin: 0;
+}
+
+/* -- Dynamic Button For Responsive Menu -------------------------------------*/
+
+/*
+The button to open/close the Menu is custom-made and not part of Pure. Here's
+how it works:
+*/
+
+/*
+`.menu-link` represents the responsive menu toggle that shows/hides on
+small screens.
+*/
+
+.menu-link {
+    position: fixed;
+    display: block;
+    /* show this only on small screens */
+    top: 0;
+    left: 0;
+    /* "#menu width" */
+    background: #000;
+    background: rgba(0,0,0,0.7);
+    font-size: 10px;
+    /* change this value to increase/decrease button size */
+    z-index: 10;
+    width: 2em;
+    height: auto;
+    padding: 2.1em 1.6em;
+}
+
+.menu-link:hover,
+.menu-link:focus {
+    background: #000;
+}
+
+.menu-link span {
+    position: relative;
+    display: block;
+}
+
+.menu-link span,
+.menu-link span:before,
+.menu-link span:after {
+    background-color: #fff;
+    width: 100%;
+    height: 0.2em;
+}
+
+.menu-link span:before,
+.menu-link span:after {
+    position: absolute;
+    margin-top: -0.6em;
+    content: " ";
+}
+
+.menu-link span:after {
+    margin-top: 0.6em;
+}
+
+/* -- Responsive Styles (Media Queries) ------------------------------------- */
+
+/*
+Hides the menu at `48em`, but modify this based on your app's needs.
+*/
+
+.header,
+.content {
+    padding-left: 2em;
+    padding-right: 2em;
+}
+
+#layout {
+    padding-left: 150px;
+    /* left col width "#menu" */
+    left: 0;
+}
+
+#menu {
+    left: 150px;
+}
+
+.menu-link {
+    position: fixed;
+    left: 150px;
+    display: none;
+}
+
+#layout.active .menu-link {
+    left: 150px;
+}
\ No newline at end of file
diff --git a/src/resources/css/side-menu.css b/src/resources/css/side-menu.css
new file mode 100644 (file)
index 0000000..b5ce7ae
--- /dev/null
@@ -0,0 +1,248 @@
+body {
+    color: #777;
+}
+
+.pure-img-responsive {
+    max-width: 100%;
+    height: auto;
+}
+
+/*
+Add transition to containers so they can push in and out.
+*/
+#layout,
+#menu,
+.menu-link {
+    -webkit-transition: all 0.2s ease-out;
+    -moz-transition: all 0.2s ease-out;
+    -ms-transition: all 0.2s ease-out;
+    -o-transition: all 0.2s ease-out;
+    transition: all 0.2s ease-out;
+}
+
+/*
+This is the parent `<div>` that contains the menu and the content area.
+*/
+#layout {
+    position: relative;
+    padding-left: 0;
+}
+    #layout.active #menu {
+        left: 150px;
+        width: 150px;
+    }
+
+    #layout.active .menu-link {
+        left: 150px;
+    }
+/*
+The content `<div>` is where all your content goes.
+*/
+.content {
+    margin: 0 auto;
+    padding: 0 2em;
+    max-width: 800px;
+    margin-bottom: 50px;
+    line-height: 1.6em;
+}
+
+.header {
+     margin: 0;
+     color: #333;
+     text-align: center;
+     padding: 2.5em 2em 0;
+     border-bottom: 1px solid #eee;
+ }
+    .header h1 {
+        margin: 0.2em 0;
+        font-size: 3em;
+        font-weight: 300;
+    }
+     .header h2 {
+        font-weight: 300;
+        color: #ccc;
+        padding: 0;
+        margin-top: 0;
+    }
+
+.content-subhead {
+    margin: 50px 0 20px 0;
+    font-weight: 300;
+    color: #888;
+}
+
+
+
+/*
+The `#menu` `<div>` is the parent `<div>` that contains the `.pure-menu` that
+appears on the left side of the page.
+*/
+
+#menu {
+    margin-left: -150px; /* "#menu" width */
+    width: 150px;
+    position: fixed;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    z-index: 1000; /* so the menu or its navicon stays above all content */
+    background: #191818;
+    overflow-y: auto;
+    -webkit-overflow-scrolling: touch;
+}
+    /*
+    All anchors inside the menu should be styled like this.
+    */
+    #menu a {
+        color: #999;
+        border: none;
+        padding: 0.6em 0 0.6em 0.6em;
+    }
+
+    /*
+    Remove all background/borders, since we are applying them to #menu.
+    */
+     #menu .pure-menu,
+     #menu .pure-menu ul {
+        border: none;
+        background: transparent;
+    }
+
+    /*
+    Add that light border to separate items into groups.
+    */
+    #menu .pure-menu ul,
+    #menu .pure-menu .menu-item-divided {
+        border-top: 1px solid #333;
+    }
+        /*
+        Change color of the anchor links on hover/focus.
+        */
+        #menu .pure-menu li a:hover,
+        #menu .pure-menu li a:focus {
+            background: #333;
+        }
+
+    /*
+    This styles the selected menu item `<li>`.
+    */
+    #menu .pure-menu-selected,
+    #menu .pure-menu-heading {
+        background: #1f8dd6;
+    }
+        /*
+        This styles a link within a selected menu item `<li>`.
+        */
+        #menu .pure-menu-selected a {
+            color: #fff;
+        }
+
+    /*
+    This styles the menu heading.
+    */
+    #menu .pure-menu-heading {
+        font-size: 110%;
+        color: #fff;
+        margin: 0;
+    }
+
+/* -- Dynamic Button For Responsive Menu -------------------------------------*/
+
+/*
+The button to open/close the Menu is custom-made and not part of Pure. Here's
+how it works:
+*/
+
+/*
+`.menu-link` represents the responsive menu toggle that shows/hides on
+small screens.
+*/
+.menu-link {
+    position: fixed;
+    display: block; /* show this only on small screens */
+    top: 0;
+    left: 0; /* "#menu width" */
+    background: #000;
+    background: rgba(0,0,0,0.7);
+    font-size: 10px; /* change this value to increase/decrease button size */
+    z-index: 10;
+    width: 2em;
+    height: auto;
+    padding: 2.1em 1.6em;
+}
+
+    .menu-link:hover,
+    .menu-link:focus {
+        background: #000;
+    }
+
+    .menu-link span {
+        position: relative;
+        display: block;
+    }
+
+    .menu-link span,
+    .menu-link span:before,
+    .menu-link span:after {
+        background-color: #fff;
+        width: 100%;
+        height: 0.2em;
+    }
+
+        .menu-link span:before,
+        .menu-link span:after {
+            position: absolute;
+            margin-top: -0.6em;
+            content: " ";
+        }
+
+        .menu-link span:after {
+            margin-top: 0.6em;
+        }
+
+
+/* -- Responsive Styles (Media Queries) ------------------------------------- */
+
+/*
+Hides the menu at `48em`, but modify this based on your app's needs.
+*/
+@media (min-width: 48em) {
+
+    .header,
+    .content {
+        padding-left: 2em;
+        padding-right: 2em;
+    }
+
+    #layout {
+        padding-left: 150px; /* left col width "#menu" */
+        left: 0;
+    }
+    #menu {
+        left: 150px;
+    }
+
+    .menu-link {
+        position: fixed;
+        left: 150px;
+        display: none;
+    }
+
+    #layout.active .menu-link {
+        left: 150px;
+    }
+}
+
+@media (max-width: 48em) {
+    /* Only apply this when the window is small. Otherwise, the following
+    case results in extra padding on the left:
+        * Make the window small.
+        * Tap the menu to trigger the active state.
+        * Make the window large again.
+    */
+    #layout.active {
+        position: relative;
+        left: 150px;
+    }
+}
+
diff --git a/src/resources/images/buildbotics_logo.png b/src/resources/images/buildbotics_logo.png
new file mode 100644 (file)
index 0000000..c6026d7
Binary files /dev/null and b/src/resources/images/buildbotics_logo.png differ
diff --git a/src/resources/js/ui.js b/src/resources/js/ui.js
new file mode 100644 (file)
index 0000000..acc38a0
--- /dev/null
@@ -0,0 +1,35 @@
+(function (window, document) {
+
+    var layout   = document.getElementById('layout'),
+        menu     = document.getElementById('menu'),
+        menuLink = document.getElementById('menuLink');
+
+    function toggleClass(element, className) {
+        var classes = element.className.split(/\s+/),
+            length = classes.length,
+            i = 0;
+
+        for(; i < length; i++) {
+          if (classes[i] === className) {
+            classes.splice(i, 1);
+            break;
+          }
+        }
+        // The className is not found
+        if (length === classes.length) {
+            classes.push(className);
+        }
+
+        element.className = classes.join(' ');
+    }
+
+    menuLink.onclick = function (e) {
+        var active = 'active';
+
+        e.preventDefault();
+        toggleClass(layout, active);
+        toggleClass(menu, active);
+        toggleClass(menuLink, active);
+    };
+
+}(this, this.document));
index aa8d798aa14fa7c070770f24186f272e3474201f..e68441612613fba11ec42221017c61afe5795482 100644 (file)
@@ -1,3 +1,52 @@
+.button-success:not([disabled])
+  background rgb(28, 184, 65)
+
+.header img
+  vertical-align top
+
+.logo
+  font-size 30pt
+  font-family Audiowide
+  display inline
+  margin-right 0.5em
+
+  .left
+    color #444
+  .right
+    color #e5aa3d
+
+
+#menu
+  .save
+    display block
+    margin 0.25em auto
+
+  .pure-menu-heading
+    background inherit
+    padding 0
+
+    .pure-menu-link
+      padding 0.6em
+      color #fff
+
+  .pure-menu-item .pure-menu-link
+    padding-left 1.5em
+
+
+#main
+  .content
+    h2
+      text-transform capitalize
+
+    .pure-control-group
+      label.units
+        text-align left
+
+    .switch
+      h3, .pure-control-group
+        display inline-block
+
+
 table.axes
   border-collapse collapse