Working GCode streaming
authorJoseph Coffland <joseph@cauldrondevelopment.com>
Fri, 5 Aug 2016 20:27:58 +0000 (13:27 -0700)
committerJoseph Coffland <joseph@cauldrondevelopment.com>
Fri, 5 Aug 2016 20:27:58 +0000 (13:27 -0700)
20 files changed:
scripts/setup_rpi.sh
src/jade/index.jade
src/jade/templates/config-view.jade [deleted file]
src/jade/templates/switches-view.jade
src/js/app.js
src/js/config-view.js [deleted file]
src/js/control-view.js
src/js/spindle-view.js
src/js/switches-view.js
src/py/bbctrl/AVR.py
src/py/bbctrl/Config.py [new file with mode: 0644]
src/py/bbctrl/Ctrl.py [new file with mode: 0644]
src/py/bbctrl/GCodeStream.py [new file with mode: 0644]
src/py/bbctrl/Jog.py
src/py/bbctrl/Web.py
src/py/bbctrl/__init__.py
src/py/bbctrl/default-config.json [deleted file]
src/resources/config-template.json
src/resources/default-config.json [new file with mode: 0644]
src/stylus/style.styl

index dd389750d21bbfa6f3f931baa3814b3afe116a03..d2c5be9acac740404078ae8c1a0c7ca76f7f61df 100755 (executable)
@@ -61,9 +61,9 @@ EOF
     update-rc.d resize2fs_once defaults
 fi
 
-# Install pacakges
+# Install packages
 apt-get install -y avahi-daemon avrdude minicom python3-pip i2c-tools
-pip-3.2 install tornado sockjs-tornado pyserial smbus
+pip-3.2 install --upgrade tornado sockjs-tornado pyserial smbus
 
 # Clean
 apt-get autoclean
@@ -84,6 +84,7 @@ sed -i 's/^\([23456]:.*\/sbin\/getty\)/#\1/' /etc/inittab
 
 # Enable I2C
 sed -i 's/#dtparam=i2c/dtparam=i2c/' /boot/config.txt
+echo 'dtparam=i2c_vc=on' >> /boot/config.txt
 echo i2c-bcm2708 >> /etc/modules
 echo i2c-dev >> /etc/modules
 
@@ -92,6 +93,6 @@ cp bbctrl.init.d /etc/init.d/bbctrl
 chmod +x /etc/init.d/bbctrl
 update-rc.d bbctrl defaults
 
-# TODO setup input and serial device permissions in udev
+# TODO setup input and serial device permissions in udev & forward 80 -> 8080
 
 reboot
index d6612659a302e24d7cda39e6aa31ee237be8de8e..eb8a8779bf682920f46bf1561046007f8cf5ddbb 100644 (file)
@@ -73,7 +73,7 @@ html(lang="en")
 
         .content(class="{{currentView}}-view")
           component(:is="currentView + '-view'", :index="index",
-            :config="config", :template="template", keep-alive)
+            :config="config", :template="template", :state="state", keep-alive)
 
     #templates
       include ../../build/templates.jade
diff --git a/src/jade/templates/config-view.jade b/src/jade/templates/config-view.jade
deleted file mode 100644 (file)
index a1747df..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-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
index e1ca46a3182093de149ba423aa9369971e46f3fc..a8dc77b2aea7d1d7033b37de5f08d6079fc0ec7c 100644 (file)
@@ -4,7 +4,5 @@ script#switches-view-template(type="text/x-template")
 
     form.pure-form.pure-form-aligned
       fieldset
-        .switch(v-for="sw in switches")
-          h3 Switch {{$index}}
-          templated-input(v-for="templ in template.switches", :name="$key",
-            :model.sync="sw[$key]", :template="templ")
+        templated-input(v-for="templ in template.switches", :name="$key",
+          :model.sync="switches[$key]", :template="templ")
index 16d4481863ca1a15e0ac53bfd8dd44474281ab9e..5fae6aecdd4a22267e82d468f5b20ad3f9d066f6 100644 (file)
@@ -15,7 +15,8 @@ module.exports = new Vue({
       index: -1,
       modified: false,
       template: {motors: {}, axes: {}},
-      config: {motors: [{}]}
+      config: {motors: [{}]},
+      state: {}
     }
   },
 
@@ -37,7 +38,10 @@ module.exports = new Vue({
 
 
     send: function (msg) {
-      if (this.status == 'connected') this.sock.send(msg)
+      if (this.status == 'connected') {
+        console.debug('>', msg);
+        this.sock.send(msg)
+      }
     },
 
 
@@ -53,14 +57,15 @@ module.exports = new Vue({
 
   methods: {
     update: function () {
-      $.get('/config-template.json').success(function (data, status, xhr) {
-        this.template = data;
-
-        api.get('load').done(function (data) {
-          this.config = data;
-          this.parse_hash();
+      $.get('/config-template.json', {cache: false})
+        .success(function (data, status, xhr) {
+          this.template = data;
+
+          api.get('load').done(function (data) {
+            this.config = data;
+            this.parse_hash();
+          }.bind(this))
         }.bind(this))
-      }.bind(this))
     },
 
 
@@ -68,7 +73,11 @@ module.exports = new Vue({
       this.sock = new Sock('//' + window.location.host + '/ws');
 
       this.sock.onmessage = function (e) {
-        this.$broadcast('message', e.data);
+        var msg = e.data;
+
+        if (typeof msg == 'object')
+          for (var key in msg)
+            this.$set('state.' + key, msg[key]);
       }.bind(this);
 
       this.sock.onopen = function (e) {
diff --git a/src/js/config-view.js b/src/js/config-view.js
deleted file mode 100644 (file)
index 0a8ea27..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-'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++;
-    }
-  }
-}
index 1fd182e411238ea5e8a1dbd1fc9f0d4bcb698521..9441bafd4682acdecfe70515a9d906c1e3f35429 100644 (file)
@@ -10,7 +10,7 @@ function is_array(x) {
 
 module.exports = {
   template: '#control-view-template',
-  props: ['config'],
+  props: ['config', 'state'],
 
 
   data: function () {
@@ -20,7 +20,6 @@ module.exports = {
       last_file: '',
       files: [],
       axes: 'xyzabc',
-      state: {},
       gcode: '',
       speed_override: 1,
       feed_override: 1
@@ -35,30 +34,9 @@ module.exports = {
 
 
   events: {
-    jog: function (axis, move) {
-      console.debug('jog(' + axis + ', ' + move + ')');
-      this.send('g91 g0' + axis + move);
-    },
-
-
-    home: function (axis) {
-      console.debug('home(' + axis + ')');
-      this.send('$home ' + axis);
-    },
-
-
-    zero: function (axis) {
-      console.debug('zero(' + axis + ')');
-      this.send('$zero ' + axis);
-    },
-
-
-    message: function (data) {
-      if (typeof data == 'object')
-        for (var key in data)
-          this.$set('state.' + key, data[key]);
-    }
-  },
+    jog: function (axis, move) {this.send('g91 g0' + axis + move)},
+    home: function (axis) {this.send('$home ' + axis)},
+    zero: function (axis) {this.send('$zero ' + axis)}  },
 
 
   ready: function () {
@@ -80,7 +58,7 @@ module.exports = {
 
 
     estop: function () {
-      this.$set('state.es', !this.state.es);
+      this.send('$es=' + (this.state.es ? 0 : 1));
     },
 
 
index 2423ff12948ca973532752c10f02039c073b0185..475b3065dd9c82106dcca1b92e09ce8aebeaf010 100644 (file)
@@ -35,8 +35,7 @@ module.exports = {
         var template = this.template.spindle;
         for (var key in template)
           if (!this.spindle.hasOwnProperty(key))
-            this.$set('spindle["' + key + '"]',
-                      template[key].default);
+            this.$set('spindle["' + key + '"]', template[key].default);
       }.bind(this));
     }
   }
index 1d4c5547509a85953bac149403b9426d22425b3e..6c0957ad1c2e73a7a57943dd290b5d549e325f71 100644 (file)
@@ -8,7 +8,7 @@ module.exports = {
 
   data: function () {
     return {
-      'switches': []
+      switches: {}
     }
   },
 
@@ -31,15 +31,12 @@ module.exports = {
       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);
-        }
+        else this.switches = {};
+
+        var template = this.template.switches;
+        for (var key in template)
+          if (!this.switches.hasOwnProperty(key))
+            this.$set('switches["' + key + '"]', template[key].default);
       }.bind(this));
     }
   }
index 21b3b3a9aa5880c04719aea7aa4157f01a8debfc..c5abb3d6860b99f040ead863aeb0ac6f2b896102 100644 (file)
 import re
 import serial
+import json
 import logging
+from collections import deque
+
+import bbctrl
 
 
 log = logging.getLogger('AVR')
 
+# These constants must be kept in sync with i2c.h from the AVR code
+I2C_NULL           = 0
+I2C_ESTOP          = 1
+I2C_PAUSE          = 2
+I2C_OPTIONAL_PAUSE = 3
+I2C_RUN            = 4
+I2C_FLUSH          = 5
+I2C_STEP           = 6
+I2C_REPORT         = 7
+I2C_HOME           = 8
+
 
 class AVR():
     def __init__(self, ctrl):
         self.ctrl = ctrl
 
+        self.vars = {}
         self.state = 'idle'
-        self.line = -1
-        self.step = 0
-        self.f = None
+        self.stream = None
+        self.queue = deque()
+        self.in_buf = ''
+        self.command = None
+        self.flush_id = 1
 
         try:
             self.sp = serial.Serial(ctrl.args.serial, ctrl.args.baud,
-                                    rtscts = 1, timeout = 0)
+                                    rtscts = 1, timeout = 0, write_timeout = 0)
+            self.sp.nonblocking()
 
         except Exception as e:
             log.warning('Failed to open serial port: %s', e)
             return
 
-        self.in_buf = ''
-        self.out_buf = None
-        self.ctrl.input_queue.put('$echo=0\n\n')
-
         ctrl.ioloop.add_handler(self.sp, self.serial_handler, ctrl.ioloop.READ)
-        ctrl.ioloop.add_handler(self.ctrl.input_queue._reader.fileno(),
-                                self.queue_handler, ctrl.ioloop.READ)
 
+        try:
+            self.i2c_bus = smbus.SMBus(ctrl.args.avr_port)
+            self.i2c_addr = ctrl.args.avr_addr
+
+         except FileNotFoundError as e:
+            self.i2c_bus = None
+            log.warning('Failed to open device: %s', e)
+
+        self.report()
+
+
+    def _state_transition_error(self, state):
+        raise Exception('Cannot %s in %s state' % (state, self.state))
+
+
+    def _state_transition(self, state, optional = False, step = False):
+        if state == self.state: return
+
+        if state == 'idle':
+            if self.stream is not None: self.stream.reset()
+
+        elif state == 'run':
+            if self.state in ['idle', 'pause'] and self.stream is not None:
+                self.set_write(True)
+
+                if step:
+                    self._i2c_command(I2C_STEP)
+                    state = 'pause'
+
+                else: self._i2c_command(I2C_RUN)
+
+            else: self._state_transition_error(state)
+
+        elif state  == 'pause'
+            if self.state == 'run':
+                if optional: self._i2c_command(I2C_OPTIONAL_PAUSE)
+                else: self._i2c_command(I2C_PAUSE)
+
+            else: self._state_transition_error(state)
+
+        elif state == 'stop':
+            if self.state in ['run', 'pause']: self._flush()
+            else: self._state_transition_error(state)
+
+        elif state == 'estop': self._i2c_command(I2C_ESTOP)
+
+        elif state == 'home':
+            if self.state == 'idle': self._i2c_command(I2C_HOME)
+            else: self._state_transition_error(state)
 
-    def close(self):
-        self.sp.close()
+        else: raise Exception('Unrecognized state "%s"' % state)
+
+        self.state = state
+
+
+    def _i2c_command(self, cmd, word = None):
+        if word is not None:
+            self.i2c_bus.write_word_data(self.i2c_addr, cmd, word)
+        self.i2c_bus.write_byte(self.i2c_addr, cmd)
+
+
+    def _flush(self):
+        if self.stream is not None: self.stream.reset()
+
+        self._i2c_command(I2C_FLUSH, word = self.flush_id)
+        self.queue_command('$end_flush %u' % self.flush_id)
+
+        self.flush_id += 1
+        if 1 << 16 <= self.flush_id: self.flush_id = 1
+
+
+    def report(self): self._i2c_command(I2C_REPORT)
+
+
+    def load_next_command(self, cmd):
+        log.info(cmd)
+        self.command = bytes(cmd.strip() + '\n', 'utf-8')
 
 
     def set_write(self, enable):
@@ -48,49 +135,36 @@ class AVR():
 
 
     def serial_write(self):
-        # Finish writing current line
-        if self.out_buf is not None:
+        # Finish writing current command
+        if self.command is not None:
             try:
-                count = self.sp.write(self.out_buf)
-                log.debug('Wrote %d', count)
+                count = self.sp.write(self.command)
+
             except Exception as e:
                 self.set_write(False)
                 raise e
 
-            self.out_buf = self.out_buf[count:]
-            if len(self.out_buf): return
-            self.out_buf = None
-
-        # Close file if stopped
-        if self.state == 'stop' and self.f is not None:
-            self.f.close()
-            self.f = None
-
-        # Read next line if running
-        if self.state == 'run':
-            # Strip white-space & comments and encode to bytearray
-            self.out_buf = self.f.readline().strip()
-            self.out_buf = re.sub(r';.*', '', self.out_buf)
-            if len(self.out_buf):
-                log.info(self.out_buf)
-                self.out_buf = bytes(self.out_buf + '\n', 'utf-8')
-            else: self.out_buf = None
-
-            # Pause if done stepping
-            if self.step:
-                self.step -= 1
-                if not self.step:
-                    self.state = 'pause'
-
-        # Stop if not longer running
-        else:
-            self.set_write(False)
-            self.step = 0
+            self.command = self.command[count:]
+            if len(self.command): return # There's more
+            self.command = None
+
+        # Load next command from queue
+        if len(self.queue): self.load_next_command(self.queue.pop())
+
+        # Load next GCode command, if running or paused
+        elif self.state in ['run', 'pause'] and self.stream is not None:
+            cmd = self.stream.next()
+
+            if cmd is None: self.set_write(False)
+            else: self.load_next_command(cmd)
+
+        # Else stop writing
+        else: self.set_write(False)
 
 
     def serial_read(self):
         try:
-            data = self.sp.read(self.sp.inWaiting())
+            data = self.sp.read(self.sp.in_waiting)
             self.in_buf += data.decode('utf-8')
 
         except Exception as e:
@@ -104,41 +178,53 @@ class AVR():
             self.in_buf = self.in_buf[i + 1:]
 
             if line:
-                self.ctrl.output_queue.put(line)
-                log.debug(line)
+                try:
+                    msg = json.loads(line)
 
+                    if 'firmware' in msg: self.report()
+                    if 'es' in msg and msg['es']: self.estop()
 
-    def queue_handler(self, fd, events):
-        if self.ctrl.input_queue.empty(): return
+                    self.vars.update(msg)
+                    self.ctrl.web.broadcast(msg)
+                    log.debug(line)
 
-        data = self.ctrl.input_queue.get()
-        self.sp.write(data.encode())
+                except Exception as e:
+                    log.error('%s, data: %s', e, line)
 
 
-    def home(self):
-        if self.state != 'idle': raise Exception('Already running')
-        # TODO
+    def queue_command(self, cmd):
+        self.queue.append(cmd)
+        self.set_write(True)
 
 
-    def start(self, path):
-        if self.f is None:
-            self.f = open('upload' + path, 'r')
-            self.line = 0
+    def load(self, path):
+        if self.stream is None:
+            self.stream = bbctrl.GCodeStream(path)
 
-        self.set_write(True)
-        self.state = 'run'
 
+    def mdi(self, cmd):
+        if self.state != 'idle':
+            raise Exception('Busy, cannot run MDI command')
+
+        self.queue_command(cmd)
+
+
+    def jog(self, axes):
+        # TODO jogging via I2C
+
+        if self.state != 'idle': raise Exception('Busy, cannot jog')
 
-    def stop(self):
-        if self.state == 'idle': return
-        self.state == 'stop'
+        axes = ["{:6.5f}".format(x) for x in axes]
+        self.queue_command('$jog ' + ' '.join(axes))
 
 
-    def pause(self, optional):
-        self.state = 'pause'
+    def set(self, index, code, value):
+        self.queue_command('${}{}={}'.format(index, code, value))
 
 
-    def step(self):
-        self.step += 1
-        if self.state == 'idle': self.start()
-        else: self.state = 'run'
+    def home(self): self._state_transition('home')
+    def start(self): self._state_transition('run')
+    def estop(self): self._state_transition('estop')
+    def stop(self): self._state_transition('stop')
+    def pause(self, opt): self._state_transition('pause', optional = opt)
+    def step(self): self._state_transition('run', step = True)
diff --git a/src/py/bbctrl/Config.py b/src/py/bbctrl/Config.py
new file mode 100644 (file)
index 0000000..50e2e5b
--- /dev/null
@@ -0,0 +1,82 @@
+import json
+import logging
+
+import bbctrl
+
+
+log = logging.getLogger('Config')
+
+
+class Config(object):
+    def __init__(self, ctrl):
+        self.ctrl = ctrl
+
+        # Load config template
+        with open(bbctrl.get_resource('http/config-template.json'), 'r',
+                  encoding = 'utf-8') as f:
+            self.template = json.load(f)
+
+
+    def load_path(self, path):
+        with open(path, 'r') as f:
+            return json.load(f)
+
+
+    def load(self):
+        try:
+            return self.load_path('config.json')
+
+        except Exception as e:
+            log.warning('%s', e)
+            return self.load_path(
+                bbctrl.get_resource('http/default-config.json'))
+
+
+    def save(self, config):
+        with open('config.json', 'w') as f:
+            json.dump(config, f)
+
+        self.update(config)
+
+        log.info('Saved')
+
+
+    def encode_cmd(self, 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
+
+        self.ctrl.avr.set(index, spec['code'], value)
+
+
+    def encode_category(self, index, config, category):
+        for key, spec in category.items():
+            if key in config:
+                self.encode_cmd(index, config[key], spec)
+
+
+    def encode(self, index, config, tmpl):
+        for category in tmpl.values():
+            self.encode_category(index, config, category)
+
+
+    def update(self, config):
+        # Motors
+        tmpl = self.template['motors']
+        for index in range(len(config['motors'])):
+            self.encode(index + 1, config['motors'][index], tmpl)
+
+        # Axes
+        tmpl = self.template['axes']
+        for axis in 'xyzabc':
+            if not axis in config['axes']: continue
+            self.encode(axis, config['axes'][axis], tmpl)
+
+        # Switches
+        tmpl = self.template['switches']
+        for index in range(len(config['switches'])):
+            self.encode_category(index + 1, config['switches'][index], tmpl)
+
+        # Spindle
+        tmpl = self.template['spindle']
+        self.encode_category('', config['spindle'], tmpl)
diff --git a/src/py/bbctrl/Ctrl.py b/src/py/bbctrl/Ctrl.py
new file mode 100644 (file)
index 0000000..97b186a
--- /dev/null
@@ -0,0 +1,18 @@
+import logging
+
+import bbctrl
+
+
+log = logging.getLogger('Ctrl')
+
+
+class Ctrl(object):
+    def __init__(self, args, ioloop):
+        self.args = args
+        self.ioloop = ioloop
+
+        self.config = bbctrl.Config(self)
+        self.web = bbctrl.Web(self)
+        self.avr = bbctrl.AVR(self)
+        self.jog = bbctrl.Jog(self)
+        self.lcd = bbctrl.LCD(self)
diff --git a/src/py/bbctrl/GCodeStream.py b/src/py/bbctrl/GCodeStream.py
new file mode 100644 (file)
index 0000000..5745f4c
--- /dev/null
@@ -0,0 +1,59 @@
+import re
+import logging
+
+
+log = logging.getLogger('GCode')
+
+
+class GCodeStream():
+    comment1RE = re.compile(r';.*')
+    comment2RE = re.compile(r'\(([^\)]*)\)')
+
+
+    def __init__(self, path):
+        self.path = path
+        self.f = None
+
+        self.open()
+
+
+    def close(self):
+        if self.f is not None:
+            self.f.close()
+            self.f = None
+
+
+    def open(self):
+        self.close()
+
+        self.line = 0
+        self.f = open('upload' + self.path, 'r')
+
+
+    def reset(self): self.open()
+
+
+    def comment(self, s):
+        log.debug('Comment: %s', s)
+
+
+    def next(self):
+        line = self.f.readline()
+        if line is None: return
+
+        # Remove comments
+        line = self.comment1RE.sub('', line)
+
+        for comment in self.comment2RE.findall(line):
+            self.comment(comment)
+
+        line = self.comment2RE.sub(' ', line)
+
+        # Remove space
+        line = line.strip()
+
+        # Append line number
+        line += ' N%d' % self.line
+        self.line += 1
+
+        return line
index ef81a0c7698e76c1dc46549ffcc342332aae9a02..6ca5db6f0e97e0929a5d743f4913ce7c2647fdbd 100644 (file)
@@ -5,6 +5,8 @@ from inevent.Constants import *
 # Listen for input events
 class Jog(inevent.JogHandler):
     def __init__(self, ctrl):
+        self.ctrl = ctrl
+
         config = {
             "deadband": 0.1,
             "axes": [ABS_X, ABS_Y, ABS_RZ, ABS_Z],
@@ -25,10 +27,7 @@ class Jog(inevent.JogHandler):
     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)
+            self.ctrl.avr.jog(self.v)
 
 
     def changed(self):
index c60ba464e816c601fe4d9e8feb78df7bce1b4000..7c55efd9e4b7ebeedc88dd55434809d85eab1520 100644 (file)
@@ -28,6 +28,10 @@ class StartHandler(bbctrl.APIHandler):
     def put_ok(self, path): self.ctrl.avr.start(path)
 
 
+class EStopHandler(bbctrl.APIHandler):
+    def put_ok(self): self.ctrl.avr.estop()
+
+
 class StopHandler(bbctrl.APIHandler):
     def put_ok(self): self.ctrl.avr.stop()
 
@@ -55,37 +59,35 @@ class OverrideSpeedHandler(bbctrl.APIHandler):
 class Connection(sockjs.tornado.SockJSConnection):
     def heartbeat(self):
         self.timer = self.ctrl.ioloop.call_later(3, self.heartbeat)
-        self.send_json({'heartbeat': self.count})
+        self.send({'heartbeat': self.count})
         self.count += 1
 
 
-    def send_json(self, data):
-        self.send(str.encode(json.dumps(data)))
-
-
     def on_open(self, info):
         self.ctrl = self.session.server.ctrl
+        self.clients = self.ctrl.web.clients
 
         self.timer = self.ctrl.ioloop.call_later(3, self.heartbeat)
         self.count = 0;
 
-        self.ctrl.clients.append(self)
-        self.send_json(self.ctrl.state)
+        self.clients.append(self)
+        self.send(self.ctrl.avr.vars)
 
 
     def on_close(self):
         self.ctrl.ioloop.remove_timeout(self.timer)
-        self.ctrl.clients.remove(self)
+        self.clients.remove(self)
 
 
     def on_message(self, data):
-        self.ctrl.input_queue.put(data + '\n')
+        self.ctrl.avr.mdi(data)
 
 
 
 class Web(tornado.web.Application):
     def __init__(self, ctrl):
         self.ctrl = ctrl
+        self.clients = []
 
         handlers = [
             (r'/api/load', LoadHandler),
@@ -93,6 +95,7 @@ class Web(tornado.web.Application):
             (r'/api/file(/.+)?', bbctrl.FileHandler),
             (r'/api/home', HomeHandler),
             (r'/api/start(/.+)', StartHandler),
+            (r'/api/estop', EStopHandler),
             (r'/api/stop', StopHandler),
             (r'/api/pause', PauseHandler),
             (r'/api/pause/optional', OptionalPauseHandler),
@@ -118,3 +121,8 @@ class Web(tornado.web.Application):
             sys.exit(1)
 
         log.info('Listening on http://%s:%d/', ctrl.args.addr, ctrl.args.port)
+
+
+    def broadcast(self, msg):
+        if self.clients:
+            self.clients[0].broadcast(self.clients, msg)
index 5418cd5d7693cdd46a70130983e5d0a7ee9b1c82..c980cd4c636257cbe9cd84b36edd2963052a1b4c 100755 (executable)
@@ -11,6 +11,7 @@ from pkg_resources import Requirement, resource_filename
 
 from bbctrl.APIHandler import APIHandler
 from bbctrl.FileHandler import FileHandler
+from bbctrl.GCodeStream import GCodeStream
 from bbctrl.Config import Config
 from bbctrl.LCD import LCD
 from bbctrl.AVR import AVR
@@ -44,6 +45,10 @@ def parse_args():
                         help = 'LCD I2C port')
     parser.add_argument('--lcd-addr', default = 0x27, type = int,
                         help = 'LCD I2C address')
+    parser.add_argument('--avr-port', default = 0, type = int,
+                        help = 'AVR I2C port')
+    parser.add_argument('--avr-addr', default = 0x2b, type = int,
+                        help = 'AVR I2C address')
     parser.add_argument('-v', '--verbose', action = 'store_true',
                         help = 'Verbose output')
     parser.add_argument('-l', '--log', metavar = "FILE",
diff --git a/src/py/bbctrl/default-config.json b/src/py/bbctrl/default-config.json
deleted file mode 100644 (file)
index 5175343..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-{
-  "motors": [
-    {
-      "motor-map": "x",
-      "step-angle": 1.8,
-      "travel-per-rev": 3.175,
-      "microsteps": 16,
-      "polarity": "normal",
-      "power-mode": "always-on",
-      "power-level": 80,
-      "stallguard": 70
-    }, {
-      "motor-map": "y"
-    }, {
-      "motor-map": "z"
-    }, {
-      "motor-map": "a"
-    }
-  ],
-
-  "axes": {
-    "x": {
-      "mode": "standard",
-      "max-velocity": 16000,
-      "max-feedrate": 16000,
-      "max-jerk": 40,
-      "min-soft-limit": 0,
-      "max-soft-limit": 150,
-      "max-homing-jerk": 80,
-      "junction-deviation": 0.05,
-      "search-velocity": 500,
-      "latch-velocity": 100,
-      "latch-backoff": 5,
-      "zero-backoff": 1
-    },
-
-    "y": {
-      "mode": "standard"
-    },
-
-    "z": {
-      "mode": "standard"
-    },
-
-    "a": {
-      "mode": "radius",
-      "max-velocity": 1000000,
-      "max-feedrate": 1000000,
-      "min-soft-limit": 0,
-      "max-soft-limit": 0
-    },
-
-    "b": {
-      "mode": "disabled"
-    },
-
-    "c": {
-      "mode": "disabled"
-    }
-  },
-
-  "switches": [
-    {"type": "normally-open"},
-    {},
-    {},
-    {},
-    {},
-    {},
-    {},
-    {}
-  ],
-
-  "spindle": {
-  }
-}
index ecfb57e0b86e3377334bdc3c83c8c4797417a58a..e89be3cff8045dce50da03bfb771dbed2ec43b7a 100644 (file)
@@ -54,7 +54,7 @@
         "type": "percent",
         "unit": "%",
         "default": 70,
-        "code": "sg"
+        "code": "th"
       }
     }
   },
         "code": "tm"
       },
       "min-switch": {
-        "type": "int",
-        "unit": "id",
-        "min": 0,
-        "max": 8,
-        "default": 0,
+        "type": "enum",
+        "values": ["disabled", "normally-open", "normally-closed"],
+        "default": "disabled",
         "code": "sn"
       },
       "max-switch": {
-        "type": "int",
-        "unit": "id",
-        "min": 0,
-        "max": 8,
-        "default": 0,
+        "type": "enum",
+        "values": ["disabled", "normally-open", "normally-closed"],
+        "default": "disabled",
         "code": "sx"
       }
     },
   },
 
   "switches": {
-    "type": {
+    "estop": {
+      "type": "enum",
+      "values": ["disabled", "normally-open", "normally-closed"],
+      "default": "disabled",
+      "code": "et"
+    },
+    "probe": {
       "type": "enum",
-      "values": ["normally-open", "normally-closed"],
-      "default": "normally-closed",
-      "code": "sw"
+      "values": ["disabled", "normally-open", "normally-closed"],
+      "default": "disabled",
+      "code": "pt"
     }
   },
 
diff --git a/src/resources/default-config.json b/src/resources/default-config.json
new file mode 100644 (file)
index 0000000..70c06b9
--- /dev/null
@@ -0,0 +1,64 @@
+{
+  "motors": [
+    {
+      "motor-map": "x",
+      "step-angle": 1.8,
+      "travel-per-rev": 3.175,
+      "microsteps": 16,
+      "polarity": "normal",
+      "power-mode": "always-on",
+      "power-level": 80,
+      "stallguard": 70
+    }, {
+      "motor-map": "y"
+    }, {
+      "motor-map": "z"
+    }, {
+      "motor-map": "a"
+    }
+  ],
+
+  "axes": {
+    "x": {
+      "mode": "standard",
+      "max-velocity": 16000,
+      "max-feedrate": 16000,
+      "max-jerk": 40,
+      "min-soft-limit": 0,
+      "max-soft-limit": 150,
+      "max-homing-jerk": 80,
+      "junction-deviation": 0.05,
+      "search-velocity": 500,
+      "latch-velocity": 100,
+      "latch-backoff": 5,
+      "zero-backoff": 1
+    },
+
+    "y": {
+      "mode": "standard"
+    },
+
+    "z": {
+      "mode": "standard"
+    },
+
+    "a": {
+      "mode": "radius",
+      "max-velocity": 1000000,
+      "max-feedrate": 1000000,
+      "min-soft-limit": 0,
+      "max-soft-limit": 0
+    },
+
+    "b": {
+      "mode": "disabled"
+    },
+
+    "c": {
+      "mode": "disabled"
+    }
+  },
+
+  "switches": {},
+  "spindle": {}
+}
index 173a3f83b7034e1ec3a2f145e46eec2fc5dc7736..085484bc2a84dcba83d581109f2537f7ca0cffa2 100644 (file)
@@ -97,6 +97,9 @@ body
       h3, .pure-control-group
         display inline-block
 
+@keyframes blink
+  50%
+    fill #ff9d00
 
 .control-view
   table
@@ -106,151 +109,146 @@ body
     td, th
       border 1px solid #ddd
 
+  .axes
+    .axis-x .name
+      color #f00
 
-.axes
-  .axis-x .name
-    color #f00
+    .axis-y .name
+      color #0f0
 
-  .axis-y .name
-    color #0f0
+    .axis-z .name
+      color #00f
 
-  .axis-z .name
-    color #00f
+    .axis-a .name
+      color #f80
 
-  .axis-a .name
-    color #f80
+    .axis-b .name
+      color #0ff
 
-  .axis-b .name
-    color #0ff
+    .axis-c .name
+      color #f0f
 
-  .axis-c .name
-    color #f0f
+    td, th
+      padding 2px
 
-  td, th
-    padding 2px
+    th
+      text-align center
 
-  th
-    text-align center
+    td
+      text-align right
+      font-family Courier
 
-  td
-    text-align right
-    font-family Courier
+    .axis
+      .name
+        text-transform capitalize
 
-  .axis
-    .name
-      text-transform capitalize
+      .name, .position
+        font-size 36pt
+        line-height 36pt
 
-    .name, .position
-      font-size 36pt
-      line-height 36pt
+  .estop
+    display inline-block
+    width 190px
+    transition 250ms
 
-@keyframes blink
-  50%
-    fill #ff9d00
+    &.active .ring
+      animation blink 2s step-start 0s infinite
 
-.estop
-  display inline-block
-  width 190px
-  transition 250ms
+    svg
+      cursor pointer
 
-  &.active .ring
-    animation blink 2s step-start 0s infinite
+      .button:hover
+        filter brightness(120%)
 
-  svg
-    cursor pointer
+  .jog
+    float right
 
-    .button:hover
-      filter brightness(120%)
+  .jog svg
+    text
+      font-family Sans
+      font-weight bold
+      stroke transparent
+      fill #444
 
-.jog
-  float right
+    .button
+      cursor pointer
+      stroke #4c4c4c
 
-.jog svg
-  text
-    font-family Sans
-    font-weight bold
-    stroke transparent
-    fill #444
+      &:hover
+        stroke #e55
 
-  .button
-    cursor pointer
-    stroke #4c4c4c
+      path
+        overflow visible
 
-    &:hover
-      stroke #e55
+    .house
+      stroke #444
+      fill #444
 
-    path
+    .ring
+      cursor pointer
       overflow visible
 
-  .house
-    stroke #444
-    fill #444
+      .button
+        stroke transparent
 
-  .ring
-    cursor pointer
-    overflow visible
+        &:hover
+          stroke #e55
 
-    .button
-      stroke transparent
+        text
+          font-size 10pt
+          text-anchor middle
 
-      &:hover
-        stroke #e55
+  .info
+    float right
+    clear right
 
-      text
-        font-size 10pt
-        text-anchor middle
+    th, td
+      padding 3px
 
-.info
-  float right
-  clear right
+    th
+      text-align right
 
-  th, td
-    padding 3px
+  .overrides
+    clear both
 
-  th
-    text-align right
+    .override
+      margin 0.5em
+      display inline-block
 
-.overrides
-  clear both
+      .percent
+        display inline-block
+        width 3em
 
-  .override
-    margin 0.5em
-    display inline-block
+      input
+        border-radius 0
+        margin -0.4em 0.5em
 
-    .percent
-      display inline-block
-      width 3em
+  .mdi
+    clear both
+    white-space nowrap
+    margin 0.5em 0
 
     input
-      border-radius 0
-      margin -0.4em 0.5em
+      width 90%
 
-.mdi
-  clear both
-  white-space nowrap
-  margin 0.5em 0
-
-  input
-    width 90%
-
-.toolbar
-  clear both
-  margin 0.5em 0
+  .toolbar
+    clear both
+    margin 0.5em 0
 
-  .spacer
-    display inline-block
-    width 1px
-    height 1px
-    margin 0 1em
-
-.gcode
-  clear both
-  border 2px inset #ccc
-  border-radius 5px
-  overflow auto
-  width 100%
-  max-width 100%
-  min-width 100%
-  height 200px
-  padding 2px
-  white-space pre
+    .spacer
+      display inline-block
+      width 1px
+      height 1px
+      margin 0 1em
+
+  .gcode
+    clear both
+    border 2px inset #ccc
+    border-radius 5px
+    overflow auto
+    width 100%
+    max-width 100%
+    min-width 100%
+    height 200px
+    padding 2px
+    white-space pre