Updates
authorJoseph Coffland <joseph@cauldrondevelopment.com>
Sat, 2 Apr 2016 06:46:42 +0000 (23:46 -0700)
committerJoseph Coffland <joseph@cauldrondevelopment.com>
Sat, 2 Apr 2016 06:46:42 +0000 (23:46 -0700)
13 files changed:
bbctrl.py
inevent/EventStream.py
src/jade/index.jade
src/jade/templates/admin-view.jade
src/jade/templates/status-view.jade
src/jade/templates/templated-input.jade
src/js/admin-view.js
src/js/api.js [new file with mode: 0644]
src/js/app.js
src/js/status-view.js
src/resources/config-template.json
src/resources/css/pure-min.css [new file with mode: 0644]
src/stylus/style.styl

index daa34575d3a9723bd2bcd4e710e15d2fef26810f..aa0da410ac5d5864da2b944e3a767f516c1d12c7 100755 (executable)
--- a/bbctrl.py
+++ b/bbctrl.py
@@ -7,12 +7,13 @@ HTTP_PORT = 8080
 HTTP_ADDR = '0.0.0.0'
 
 import os
-from tornado import web, ioloop
+from tornado import web, ioloop, escape
 from sockjs.tornado import SockJSRouter, SockJSConnection
 import json
 import serial
 import multiprocessing
 import time
+import select
 
 import inevent
 from inevent.Constants import *
@@ -29,6 +30,11 @@ config = {
     "verbose": False
     }
 
+
+with open('http/config-template.json', 'r') as f:
+    config_template = json.load(f)
+
+
 state = {}
 clients = []
 
@@ -36,6 +42,102 @@ input_queue = multiprocessing.Queue()
 output_queue = multiprocessing.Queue()
 
 
+def encode_cmd(index, value, spec):
+    if spec['type'] == 'enum': value = spec['values'].index(value)
+    elif spec['type'] == 'bool': value = 1 if value else 0
+    elif spec['type'] == 'percent': value /= 100.0
+
+    cmd = '${}{}={}'.format(index, spec['code'], value)
+    input_queue.put(cmd + '\n')
+    #print(cmd)
+
+
+def encode_config_category(index, config, category):
+    for key, spec in category.items():
+        if key in config:
+            encode_cmd(index, config[key], spec)
+
+
+def encode_config(index, config, tmpl):
+    for category in tmpl.values():
+        encode_config_category(index, config, category)
+
+
+def update_config(config):
+    # Motors
+    tmpl = config_template['motors']
+    for index in range(len(config['motors'])):
+        encode_config(index + 1, config['motors'][index], tmpl)
+
+    # Axes
+    tmpl = config_template['axes']
+    axes = 'xyzabc'
+    for axis in axes:
+        if not axis in config['axes']: continue
+        encode_config(axis, config['axes'][axis], tmpl)
+
+    # Switches
+    tmpl = config_template['switches']
+    for index in range(len(config['switches'])):
+        encode_config_category(index + 1, config['switches'][index], tmpl)
+
+    # Spindle
+    tmpl = config_template['spindle']
+    encode_config_category('', config['spindle'], tmpl)
+
+
+
+class APIHandler(web.RequestHandler):
+    def prepare(self):
+        self.json = {}
+
+        if self.request.body:
+            try:
+                self.json = escape.json_decode(self.request.body)
+            except ValueError:
+                self.send_error(400, message = 'Unable to parse JSON.')
+
+
+    def set_default_headers(self):
+        self.set_header('Content-Type', 'application/json')
+
+
+    def write_error(self, status_code, **kwargs):
+        e = {}
+        e['message'] = str(kwargs['exc_info'][1])
+        e['code'] = status_code
+
+        self.write_json(e)
+
+
+    def write_json(self, data):
+        self.write(json.dumps(data))
+
+
+class LoadHandler(APIHandler):
+    def send_file(self, path):
+        with open(path, 'r') as f:
+            self.write_json(json.load(f))
+
+    def get(self):
+        try:
+            self.send_file('config.json')
+        except Exception as e:
+            print(e)
+            self.send_file('http/default-config.json')
+
+
+class SaveHandler(APIHandler):
+    def post(self):
+        with open('config.json', 'w') as f:
+            json.dump(self.json, f)
+
+        update_config(self.json)
+        print('Saved config')
+        self.write_json('ok')
+
+
+
 class SerialProcess(multiprocessing.Process):
     def __init__(self, input_queue, output_queue):
         multiprocessing.Process.__init__(self)
@@ -61,6 +163,9 @@ class SerialProcess(multiprocessing.Process):
         self.sp.flushInput()
 
         while True:
+            fds = [self.input_queue._reader.fileno(), self.sp]
+            ready = select.select(fds, [], [], 0.25)[0]
+
             # look for incoming tornado request
             if not self.input_queue.empty():
                 data = self.input_queue.get()
@@ -69,7 +174,7 @@ class SerialProcess(multiprocessing.Process):
                 self.writeSerial(data)
 
             # look for incoming serial data
-            if (self.sp.inWaiting() > 0):
+            if self.sp.inWaiting() > 0:
                 data = self.readSerial()
                 # send it back to tornado
                 self.output_queue.put(data)
@@ -95,7 +200,7 @@ class Connection(SockJSConnection):
 
 
 
-# check the queue for pending messages, and rely that to all connected clients
+# check the queue for pending messages, and relay them to all connected clients
 def checkQueue():
     while not output_queue.empty():
         try:
@@ -106,6 +211,8 @@ def checkQueue():
 
 
 handlers = [
+    (r'/api/load', LoadHandler),
+    (r'/api/save', SaveHandler),
     (r'/(.*)', web.StaticFileHandler,
      {'path': os.path.join(DIR, 'http/'),
       "default_filename": "index.html"}),
index ab4b7e1dfc196f76c8efaa243942d2b6d2003a24..408e5ecb93f7aabc13b9d839fa46c2deda227a94 100644 (file)
@@ -134,7 +134,7 @@ class EventStream(object):
 
 
   def __iter__(self):
-    """
+    """s
     Required to make this class an iterator
     """
     return self
index 6431dd6f8cc886078278f6f545c88274c9aaf854..294e2f886ef5e74c21253a18331eb9823b21145a 100644 (file)
@@ -9,8 +9,7 @@ html(lang="en")
 
     title Buildbotics Controller - Web interface
 
-    link(rel="stylesheet",
-      href="http://yui.yahooapis.com/pure/0.6.0/pure-min.css")
+    link(rel="stylesheet", href="/css/pure-min.css")
     //if lte IE 8
       link(rel="stylesheet", href="css/side-menu-old-ie.css")
     // [if gt IE 8] <!
@@ -19,14 +18,13 @@ html(lang="en")
 
     - 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(href="//fonts.googleapis.com/css?family=Audiowide" rel="stylesheet"
+      type="text/css")
 
-    link(rel='stylesheet' href='/css/style-' + css_hash + '.css')
+    link(rel="stylesheet" href="/css/style-" + css_hash + ".css")
 
 
-  body
+  body(v-cloak)
     #layout
       a#menuLink.menu-link(href="#menu"): span
 
@@ -82,8 +80,6 @@ html(lang="en")
     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')
 
index 46c18f1e48f9e5bbf185c70e267a243205a4576e..853214db38658577b7672c1887a17db9af2229d5 100644 (file)
@@ -1,4 +1,15 @@
 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
+  h2 Backup configuration
+  button.pure-button.pure-button-primary(@click="backup") Backup
+
+  h2 Restore configuration
+  button.pure-button.pure-button-primary(@click="restore") Restore
+
+  h2 Reset to default configuration
+  button.pure-button.pure-button-primary(@click="reset") Reset
+
+  h2 Check for new firmware
+  button.pure-button.pure-button-primary(@click="check") Check
+
+  h2 Upgrade firmware
+  button.pure-button.pure-button-primary(@click="upgrade") Upgrade
index 037f5b4bd529f9239673b2db5ee88f2cf6265d2b..837b5e779aee32825b45db6bcbbff0f165fca18b 100644 (file)
@@ -1,43 +1,30 @@
 script#status-view-template(type="text/x-template")
-  table
+  h2 Jog
+  table.jog
     tr
-      td
-      td: button(@click="jog('y', 1)") Y+
-      td
-      td: button(@click="jog('z', 1)") Z+
-      td: button(@click="jog('a', 1)") A+
+      td: button.pure-button(@click="jog('x', 1)") X+
+      td: button.pure-button(@click="jog('y', 1)") Y+
+      td: button.pure-button(@click="jog('z', 1)") Z+
+      td: button.pure-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-
+      td: button.pure-button(@click="jog('x', -1)") X-
+      td: button.pure-button(@click="jog('y', -1)") Y-
+      td: button.pure-button(@click="jog('z', -1)") Z-
+      td: button.pure-button(@click="jog('a', -1)") A-
 
-  h2 Velocity {{state.vel}}
+  h2 Velocity {{state.v}}
   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")
+        td {{state.#{axis}p}}
+        td {{state.#{axis}sf}}
+        td {{state.#{axis}sgv}}
index 26200a3e3f653d61b1df17312ad8a4e70c83e501..855d91d131fcfa51a44dcb3f8c8e8d78855cdccf 100644 (file)
@@ -2,7 +2,7 @@ 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",
+    select(v-if="template.type == 'enum' || template.values", v-model="model",
       :id="name", @change="change")
       option(v-for="opt in template.values", :value="opt") {{opt}}
 
@@ -10,12 +10,12 @@ script#templated-input-template(type="text/x-template")
       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")
+      :min="template.min", :max="template.max", :step="template.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 == 'int' && !template.values", 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")
@@ -23,4 +23,9 @@ script#templated-input-template(type="text/x-template")
     textarea(v-if="template.type == 'text'", v-model="model",
       :id="name", @change="change")
 
+    span.range(v-if="template.type == 'percent'")
+      input(type="range", v-model="model", :id="name", number, min="0",
+        max="100", step="1", @change="change")
+      | {{model}}
+
     label.units {{template.unit}}
index 96be9eb4d48be088ccbcf7d18991f0bbe3838c0e..bdf355ae68fb727b5bbdbeb6903b1b39bd0a1aac 100644 (file)
@@ -12,10 +12,27 @@ module.exports = {
 
   methods: {
     backup: function () {
+      alert('Not yet implemented');
     },
 
 
     restore: function () {
+      alert('Not yet implemented');
+    },
+
+
+    reset: function () {
+      alert('Not yet implemented');
+    },
+
+
+    check: function () {
+      alert('Not yet implemented');
+    },
+
+
+    upgrade: function () {
+      alert('Not yet implemented');
     }
   }
 }
diff --git a/src/js/api.js b/src/js/api.js
new file mode 100644 (file)
index 0000000..ca9dbc9
--- /dev/null
@@ -0,0 +1,51 @@
+'use strict'
+
+
+function api_cb(method, url, data, config) {
+  config = $.extend({
+    type: method,
+    url: '/api/' + url,
+    dataType: 'json'
+  }, config);
+
+  if (typeof data == 'object') {
+    config.data = JSON.stringify(data);
+    config.contentType = 'application/json; charset=utf-8';
+  }
+
+  var d = $.Deferred();
+
+  $.ajax(config).success(function (data, status, xhr) {
+    d.resolve(data, status, xhr);
+
+  }).error(function (xhr, status, error) {
+    var text = xhr.responseText;
+    try {text = $.parseJSON(xhr.responseText)} catch(e) {}
+    d.reject(text, xhr, status, error);
+    console.debug('API Error: ' + url + ': ' + text);
+  })
+
+  return d.promise();
+}
+
+
+module.exports = {
+  get: function (url, config) {
+    return api_cb('GET', url, undefined, config);
+  },
+
+
+  put: function(url, data, config) {
+    return api_cb('PUT', url, data, config);
+  },
+
+
+  post: function(url, data, config) {
+    return api_cb('POST', url, data, config);
+  },
+
+
+  'delete': function (url, config) {
+    return api_cb('DELETE', url, undefined, config);
+  }
+}
index 1f2438644706a0d130bdb51183e7606c890f960b..9b4705c5b751575d08586a7e929f6d73b059eb0f 100644 (file)
@@ -1,5 +1,7 @@
 'use strict'
 
+var api = require('./api');
+
 
 module.exports = new Vue({
   el: 'body',
@@ -37,7 +39,7 @@ module.exports = new Vue({
     $.get('/config-template.json').success(function (data, status, xhr) {
       this.template = data;
 
-      $.get('/default-config.json').success(function (data, status, xhr) {
+      api.get('load').done(function (data) {
         this.config = data;
 
         this.parse_hash();
@@ -50,6 +52,12 @@ module.exports = new Vue({
   methods: {
     parse_hash: function () {
       var hash = location.hash.substr(1);
+
+      if (!hash.trim().length) {
+        location.hash = 'status';
+        return;
+      }
+
       var parts = hash.split(':');
 
       if (parts.length == 2) this.index = parts[1];
@@ -59,7 +67,11 @@ module.exports = new Vue({
 
 
     save: function () {
-      this.modified = false;
+      api.post('save', this.config).done(function (data) {
+        this.modified = false;
+      }.bind(this)).fail(function (xhr, status) {
+        alert('Save failed: ' + status + ': ' + xhr.responseText);
+      });
     }
   }
 })
index 1cc7bef5be4f9126cfd0c7fe8b5449649d4c4b46..55242197dd12db721a8e00746ab678f7c62c7708 100644 (file)
@@ -14,27 +14,13 @@ module.exports = {
     return {
       axes: 'xyza',
       state: {
-        dcurx: 1, dcury: 1, dcurz: 1, dcura: 1
-        //sguardx: 1, sguardy: 1, sguardz: 1, sguarda: 1
+        xpl: 1, ypl: 1, zpl: 1, apl: 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');
 
@@ -46,7 +32,7 @@ module.exports = {
         this.$set('state.' + key, data[key]);
 
         for (var axis of ['x', 'y', 'z', 'a'])
-          if (key == 'dcur' + axis &&
+          if (key == axis + 'pl' &&
               typeof this.$get('current' + axis) == 'undefined')
             this.$set('current' + axis, (32 * data[key]).toFixed());
       }
@@ -61,17 +47,16 @@ module.exports = {
 
 
     jog: function (axis, dir) {
-      var pos = this.state['pos' + axis] + dir * this.step;
-      this.sock.send('g0' + axis + pos);
+      this.sock.send('g91 g0' + axis + (dir * this.step));
     },
 
 
     current: function (axis, value) {
       var x = value / 32.0;
-      if (this.state['dcur' + axis] == x) return;
+      if (this.state[axis + 'pl'] == x) return;
 
       var data = {};
-      data['dcur' + axis] = x;
+      data[axis + 'pl'] = x;
       this.send(data);
     }
   },
index 35f3f005785a8381c9849747e186025c689a83f5..4f0fcc3d1ca88d2cbdb5fe3782344d2f79aec50d 100644 (file)
@@ -1,34 +1,99 @@
 {
+  "motors": {
+    "general": {
+      "motor-map": {
+        "type": "enum",
+        "values": ["x", "y", "z", "a", "b", "c"],
+        "default": "x",
+        "code": "ma"
+      },
+      "step-angle": {
+        "type": "float",
+        "min": 0,
+        "max": 360,
+        "step": 0.1,
+        "unit": "degrees",
+        "default": 1.8,
+        "code": "sa"
+      },
+      "travel-per-rev": {
+        "type": "float",
+        "unit": "mm",
+        "default": 3.175,
+        "code": "tr"
+      },
+      "microsteps": {
+        "type": "int",
+        "values": [1, 2, 4, 8, 16, 32, 64, 128, 256],
+        "unit": "per full step",
+        "default": 16,
+        "code": "mi"
+      },
+      "polarity": {
+        "type": "enum",
+        "values": ["normal", "reversed"],
+        "default": "normal",
+        "code": "po"
+      }
+    },
+
+    "power": {
+      "power-mode": {
+        "type": "enum",
+        "values": ["disabled", "always-on", "in-cycle", "when-moving"],
+        "default": "in-cycle",
+        "code": "pm"
+      },
+      "power-level": {
+        "type": "percent",
+        "unit": "%",
+        "default": 80,
+        "code": "pl"
+      },
+      "stallguard": {
+        "type": "percent",
+        "unit": "%",
+        "default": 70,
+        "code": "sg"
+      }
+    }
+  },
+
   "axes": {
     "motion": {
       "mode": {
         "type": "enum",
-        "values": ["standard", "radius", "disabled"],
-        "default": "disabled"
+        "values": ["disabled", "standard", "inhibited", "radius"],
+        "default": "disabled",
+        "code": "am"
       },
       "max-velocity": {
         "type": "float",
         "min": 0,
         "unit": "mm/min",
-        "default": 16000
+        "default": 16000,
+        "code": "vm"
       },
       "max-feedrate": {
         "type": "float",
         "min": 0, "unit":
         "mm/min",
-        "default": 16000
+        "default": 16000,
+        "code": "fr"
       },
       "max-jerk": {
         "type": "float",
         "min": 0,
-        "unit": "km/min^3",
-        "default": 20
+        "unit": "mm/min³",
+        "default": 20,
+        "code": "jm"
       },
       "junction-deviation": {
         "type": "float",
         "min": 0,
         "unit": "mm",
-        "default": 0.05
+        "default": 0.05,
+        "code": "jd"
       }
     },
 
       "min-soft-limit": {
         "type": "float",
         "unit": "mm",
-        "default": 0
+        "default": 0,
+        "code": "tn"
       },
       "max-soft-limit": {
         "type": "float",
         "unit": "mm",
-        "default": 150
+        "default": 150,
+        "code": "tm"
       },
       "min-switch": {
         "type": "int",
         "unit": "id",
         "min": 0,
         "max": 8,
-        "default": 0
+        "default": 0,
+        "code": "sn"
       },
       "max-switch": {
         "type": "int",
         "unit": "id",
         "min": 0,
         "max": 8,
-        "default": 0
+        "default": 0,
+        "code": "sx"
       }
     },
 
       "max-homing-jerk": {
         "type": "float",
         "min": 0,
-        "unit": "km/min^3",
-        "default": 40
+        "unit": "mm/min³",
+        "default": 40,
+        "code": "jh"
       },
       "search-velocity": {
         "type": "float",
         "min": 0,
         "unit": "mm/min",
-        "default": 500
+        "default": 500,
+        "code": "sv"
       },
       "latch-velocity": {
         "type": "float",
         "min": 0,
         "unit": "mm/min",
-        "default": 100
+        "default": 100,
+        "code": "lv"
       },
       "latch-backoff": {
         "type": "float",
         "min": 0,
         "unit": "mm",
-        "default": 5
+        "default": 5,
+        "code": "lb"
       },
       "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
+        "default": 1,
+        "code": "zb"
       }
     }
   },
 
   "spindle": {
-    "max-speed": {
+    "spindle-type": {
+      "type": "enum",
+      "values": ["RS485", "PWM"],
+      "default": "RS485",
+      "code": "st"
+    },
+    "spin-polarity": {
+      "type": "enum",
+      "values": ["normal", "reversed"],
+      "default": "normal",
+        "code": "sp"
+    },
+    "max-spin": {
       "type": "float",
       "unit": "RPM",
       "min": 0,
-      "default": 10000
-    },
-    "type": {
-      "type": "enum",
-      "values": ["RS485", "PWM"],
-      "default": "RS485"
+      "default": 10000,
+      "code": "ss"
     },
-    "min-pulse-width": {
+    "spin-min-pulse": {
       "type": "float",
       "unit": "ms",
-      "default": 20
+      "default": 20,
+      "code": "np"
     },
-    "max-pulse-width": {
+    "spin-max-pulse": {
       "type": "float",
       "unit": "ms",
-      "default": 100
+      "default": 100,
+      "code": "mp"
     },
-    "polarity": {
-      "type": "enum",
-      "values": ["normal", "reversed"],
-      "default": "normal"
-    },
-    "ramp-up-velocity": {
+    "spin-up-velocity": {
       "type": "float",
-      "unit": "rev/min^2",
+      "unit": "rev/min²",
       "min": 0,
-      "default": 48000
+      "default": 48000,
+      "code": "su"
     },
-    "ramp-down-velocity": {
+    "spin-down-velocity": {
       "type": "float",
-      "unit": "rev/min^2",
+      "unit": "rev/min²",
       "min": 0,
-      "default": 48000
+      "default": 48000,
+      "code": "sd"
     }
   },
 
     "type": {
       "type": "enum",
       "values": ["normally-open", "normally-closed"],
-      "default": "normally-closed"
+      "default": "normally-closed",
+      "code": "sw"
     }
   },
 
   "gcode": {
-    "preamble": {"type": "text"},
-    "tool-change": {"type": "text"},
-    "epilogue": {"type": "text"}
+    "preamble": {
+      "type": "text",
+      "default": "G21 (Operate in millimeters)\nG90 (Absolute distance mode)\nG17 (Select XY plane)\nG40 (Radius compensation off)\nG49 (Tool length compensation off)\nG61 (Exact path mode)\n"
+    },
+    "tool-change": {
+      "type": "text",
+      "default": "M6 (Tool change)\n"
+    },
+    "epilogue": {
+      "type": "text",
+      "default": "M2 (End program)\n"
+    }
   }
 }
diff --git a/src/resources/css/pure-min.css b/src/resources/css/pure-min.css
new file mode 100644 (file)
index 0000000..f0aa374
--- /dev/null
@@ -0,0 +1,11 @@
+/*!
+Pure v0.6.0
+Copyright 2014 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+https://github.com/yahoo/pure/blob/master/LICENSE.md
+*/
+/*!
+normalize.css v^3.0 | MIT License | git.io/normalize
+Copyright (c) Nicolas Gallagher and Jonathan Neal
+*/
+/*! normalize.css v3.0.2 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{letter-spacing:-.31em;*letter-spacing:normal;*word-spacing:-.43em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-flex;-webkit-flex-flow:row wrap;display:-ms-flexbox;-ms-flex-flow:row wrap;-ms-align-content:flex-start;-webkit-align-content:flex-start;align-content:flex-start}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-g [class *="pure-u"]{font-family:sans-serif}.pure-u-1,.pure-u-1-1,.pure-u-1-2,.pure-u-1-3,.pure-u-2-3,.pure-u-1-4,.pure-u-3-4,.pure-u-1-5,.pure-u-2-5,.pure-u-3-5,.pure-u-4-5,.pure-u-5-5,.pure-u-1-6,.pure-u-5-6,.pure-u-1-8,.pure-u-3-8,.pure-u-5-8,.pure-u-7-8,.pure-u-1-12,.pure-u-5-12,.pure-u-7-12,.pure-u-11-12,.pure-u-1-24,.pure-u-2-24,.pure-u-3-24,.pure-u-4-24,.pure-u-5-24,.pure-u-6-24,.pure-u-7-24,.pure-u-8-24,.pure-u-9-24,.pure-u-10-24,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%;*width:4.1357%}.pure-u-1-12,.pure-u-2-24{width:8.3333%;*width:8.3023%}.pure-u-1-8,.pure-u-3-24{width:12.5%;*width:12.469%}.pure-u-1-6,.pure-u-4-24{width:16.6667%;*width:16.6357%}.pure-u-1-5{width:20%;*width:19.969%}.pure-u-5-24{width:20.8333%;*width:20.8023%}.pure-u-1-4,.pure-u-6-24{width:25%;*width:24.969%}.pure-u-7-24{width:29.1667%;*width:29.1357%}.pure-u-1-3,.pure-u-8-24{width:33.3333%;*width:33.3023%}.pure-u-3-8,.pure-u-9-24{width:37.5%;*width:37.469%}.pure-u-2-5{width:40%;*width:39.969%}.pure-u-5-12,.pure-u-10-24{width:41.6667%;*width:41.6357%}.pure-u-11-24{width:45.8333%;*width:45.8023%}.pure-u-1-2,.pure-u-12-24{width:50%;*width:49.969%}.pure-u-13-24{width:54.1667%;*width:54.1357%}.pure-u-7-12,.pure-u-14-24{width:58.3333%;*width:58.3023%}.pure-u-3-5{width:60%;*width:59.969%}.pure-u-5-8,.pure-u-15-24{width:62.5%;*width:62.469%}.pure-u-2-3,.pure-u-16-24{width:66.6667%;*width:66.6357%}.pure-u-17-24{width:70.8333%;*width:70.8023%}.pure-u-3-4,.pure-u-18-24{width:75%;*width:74.969%}.pure-u-19-24{width:79.1667%;*width:79.1357%}.pure-u-4-5{width:80%;*width:79.969%}.pure-u-5-6,.pure-u-20-24{width:83.3333%;*width:83.3023%}.pure-u-7-8,.pure-u-21-24{width:87.5%;*width:87.469%}.pure-u-11-12,.pure-u-22-24{width:91.6667%;*width:91.6357%}.pure-u-23-24{width:95.8333%;*width:95.8023%}.pure-u-1,.pure-u-1-1,.pure-u-5-5,.pure-u-24-24{width:100%}.pure-button{display:inline-block;zoom:1;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:#444;color:rgba(0,0,0,.8);border:1px solid #999;border:0 rgba(0,0,0,0);background-color:#E6E6E6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:hover,.pure-button:focus{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#1a000000', GradientType=0);background-image:-webkit-gradient(linear,0 0,0 100%,from(transparent),color-stop(40%,rgba(0,0,0,.05)),to(rgba(0,0,0,.1)));background-image:-webkit-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:-moz-linear-gradient(top,rgba(0,0,0,.05) 0,rgba(0,0,0,.1));background-image:-o-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000\9}.pure-button[disabled],.pure-button-disabled,.pure-button-disabled:hover,.pure-button-disabled:focus,.pure-button-disabled:active{border:0;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);filter:alpha(opacity=40);-khtml-opacity:.4;-moz-opacity:.4;opacity:.4;cursor:not-allowed;box-shadow:none}.pure-button-hidden{display:none}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=text]:focus,.pure-form input[type=password]:focus,.pure-form input[type=email]:focus,.pure-form input[type=url]:focus,.pure-form input[type=date]:focus,.pure-form input[type=month]:focus,.pure-form input[type=time]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=week]:focus,.pure-form input[type=number]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=color]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129FEA}.pure-form input:not([type]):focus{outline:0;border-color:#129FEA}.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus,.pure-form input[type=checkbox]:focus{outline:thin solid #129FEA;outline:1px auto #129FEA}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=text][disabled],.pure-form input[type=password][disabled],.pure-form input[type=email][disabled],.pure-form input[type=url][disabled],.pure-form input[type=date][disabled],.pure-form input[type=month][disabled],.pure-form input[type=time][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=week][disabled],.pure-form input[type=number][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=color][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form textarea:focus:invalid,.pure-form select:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus,.pure-form input[type=checkbox]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=text],.pure-form-stacked input[type=password],.pure-form-stacked input[type=email],.pure-form-stacked input[type=url],.pure-form-stacked input[type=date],.pure-form-stacked input[type=month],.pure-form-stacked input[type=time],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=week],.pure-form-stacked input[type=number],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=color],.pure-form-stacked input[type=file],.pure-form-stacked select,.pure-form-stacked label,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned input,.pure-form-aligned textarea,.pure-form-aligned select,.pure-form-aligned .pure-help-inline,.pure-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form input.pure-input-rounded,.pure-form .pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form .pure-help-inline,.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=text],.pure-group input[type=password],.pure-group input[type=email],.pure-group input[type=url],.pure-group input[type=date],.pure-group input[type=month],.pure-group input[type=time],.pure-group input[type=datetime],.pure-group input[type=datetime-local],.pure-group input[type=week],.pure-group input[type=number],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=color]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0}.pure-form .pure-help-inline,.pure-form-message-inline,.pure-form-message{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-list,.pure-menu-item{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-link,.pure-menu-heading{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-separator{display:inline-block;*display:inline;zoom:1;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-allow-hover:hover>.pure-menu-children,.pure-menu-active>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;padding:.5em 0}.pure-menu-horizontal.pure-menu-scrollable::-webkit-scrollbar{display:none}.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-link,.pure-menu-disabled,.pure-menu-heading{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent}.pure-menu-active>.pure-menu-link,.pure-menu-link:hover,.pure-menu-link:focus{background-color:#eee}.pure-menu-selected .pure-menu-link,.pure-menu-selected .pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table td:first-child,.pure-table th:first-child{border-left-width:0}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0}
\ No newline at end of file
index e68441612613fba11ec42221017c61afe5795482..70d9fdfa4ed9d046e395596b402e6cc681fcd43f 100644 (file)
@@ -1,3 +1,9 @@
+body
+  overflow-y scroll
+
+[v-cloak]
+  display none
+
 .button-success:not([disabled])
   background rgb(28, 184, 65)
 
     color #e5aa3d
 
 
+.jog
+  button
+    width 3.5em
+    margin 0.25em
+
 #menu
   .save
     display block
       label.units
         text-align left
 
+      textarea
+        width 24em
+        height 12em
+
     .switch
       h3, .pure-control-group
         display inline-block
 
 
+
 table.axes
   border-collapse collapse