Basic GCode playback
authorJoseph Coffland <joseph@cauldrondevelopment.com>
Sat, 25 Jun 2016 22:57:54 +0000 (15:57 -0700)
committerJoseph Coffland <joseph@cauldrondevelopment.com>
Sat, 25 Jun 2016 22:57:54 +0000 (15:57 -0700)
src/jade/templates/control-view.jade
src/js/api.js
src/js/app.js
src/js/control-view.js
src/py/bbctrl/APIHandler.py
src/py/bbctrl/AVR.py
src/py/bbctrl/FileHandler.py
src/py/bbctrl/Jog.py
src/py/bbctrl/LCD.py
src/py/bbctrl/Web.py
src/py/bbctrl/__init__.py

index 05b43fe82aa53f3be3b0974c9dd933293ae6370e..0bee1d647402d76d5be5e7b783dd00b80dc4ea10 100644 (file)
@@ -69,13 +69,13 @@ script#control-view-template(type="text/x-template")
       | Override:
       .override
         label Feed
-        input(title="Feed rate override.", type="range", min="-1", max="1",
+        input(title="Feed rate override.", type="range", min="0", max="2",
           step="0.01", v-model="feed_override", @change="override_feed")
         span.percent {{feed_override | percent 0}}
 
       .override
         label Speed
-        input(title="Speed override.", type="range", min="-1", max="1",
+        input(title="Speed override.", type="range", min="0", max="2",
           step="0.01", v-model="speed_override", @change="override_speed")
         span.percent {{speed_override | percent 0}}
 
@@ -94,7 +94,7 @@ script#control-view-template(type="text/x-template")
         .fa.fa-home
 
       button.pure-button(title="{{running ? 'Pause' : 'Start'}} program.",
-        @click="play_pause()", :disabled="!file")
+        @click="start_pause", :disabled="!file")
         .fa(:class="running ? 'fa-pause' : 'fa-play'")
 
       button.pure-button(title="Stop program.", @click="stop",
@@ -102,7 +102,7 @@ script#control-view-template(type="text/x-template")
         .fa.fa-stop
 
       button.pure-button(title="Pause program at next optional stop (M1).",
-        @click="optional_stop", :disabled="!file")
+        @click="optional_pause", :disabled="!file")
         .fa.fa-stop-circle-o
 
       button.pure-button(title="Execute one program step.", @click="step",
index 8b26cd7b177530c6fc2befb84b6d96b07dd2ca89..ce8424a0cce347b6534004faf14f94372ca78d1a 100644 (file)
@@ -53,7 +53,7 @@ module.exports = {
       data: data
     }, config);
 
-    return api_cb('POST', url, undefined, config);
+    return api_cb('PUT', url, undefined, config);
   },
 
 
index 3de47558d8501b2462653f05f99162e0f6b4eadd..16d4481863ca1a15e0ac53bfd8dd44474281ab9e 100644 (file)
@@ -101,7 +101,7 @@ module.exports = new Vue({
 
 
     save: function () {
-      api.post('save', this.config).done(function (data) {
+      api.put('save', this.config).done(function (data) {
         this.modified = false;
       }.bind(this)).fail(function (xhr, status) {
         alert('Save failed: ' + status + ': ' + xhr.responseText);
index 6d7a160e83dbef6c536aaafd9ebd936b4104b046..1fd182e411238ea5e8a1dbd1fc9f0d4bcb698521 100644 (file)
@@ -22,8 +22,8 @@ module.exports = {
       axes: 'xyzabc',
       state: {},
       gcode: '',
-      speed_override: 0,
-      feed_override: 0
+      speed_override: 1,
+      feed_override: 1
     }
   },
 
@@ -150,8 +150,34 @@ module.exports = {
     },
 
 
-    run: function (file) {
-      api.put('file/' + file).done(this.update);
+    home: function () {api.put('home').done(this.update)},
+
+
+    start_pause: function () {
+      if (this.running) this.pause();
+      else this.start();
+    },
+
+
+    start: function () {
+      if (!this.file) return;
+      api.put('start/' + this.file).done(this.update);
+    },
+
+
+    pause: function () {api.put('pause').done(this.update)},
+    optional_pause: function () {api.put('pause/optional').done(this.update)},
+    stop: function () {api.put('stop').done(this.update)},
+    step: function () {api.put('step').done(this.update)},
+
+
+    override_feed: function () {
+      api.put('override/feed/' + this.feed_override).done(this.update)
+    },
+
+
+    override_speed: function () {
+      api.put('override/speed/' + this.speed_override).done(this.update)
     },
 
 
@@ -162,18 +188,6 @@ module.exports = {
       var data = {};
       data[axis + 'pl'] = x;
       this.send(JSON.stringify(data));
-    },
-
-
-    override_feed: function () {},
-    override_speed: function () {},
-    step: function () {},
-    stop: function () {},
-    optional_stop: function () {},
-
-
-    home: function () {
-      this.send('$calibrate');
     }
   },
 
index dcfe7a15653c80b82ee68777c69cfba4976edd3f..6317c17be0b1924f59c7ed5164d275dd10600d2f 100644 (file)
@@ -1,8 +1,30 @@
 import json
 import tornado.web
+import tornado.httpclient
 
 
 class APIHandler(tornado.web.RequestHandler):
+    def __init__(self, app, request, **kwargs):
+        super(APIHandler, self).__init__(app, request, **kwargs)
+        self.ctrl = app.ctrl
+
+
+    def delete(self, *args, **kwargs):
+        self.delete_ok(*args, **kwargs)
+        self.write_json('ok')
+
+
+    def delete_ok(self): raise tornado.httpclient.HTTPError(405)
+
+
+    def put(self, *args, **kwargs):
+        self.put_ok(*args, **kwargs)
+        self.write_json('ok')
+
+
+    def put_ok(self): raise tornado.httpclient.HTTPError(405)
+
+
     def prepare(self):
         self.json = {}
 
index df5e39511ec8848995a807b2a5e0651c541e638a..21b3b3a9aa5880c04719aea7aa4157f01a8debfc 100644 (file)
@@ -1,3 +1,4 @@
+import re
 import serial
 import logging
 
@@ -6,29 +7,88 @@ log = logging.getLogger('AVR')
 
 
 class AVR():
-    def __init__(self, port, baud, ioloop, app):
-        self.app = app
+    def __init__(self, ctrl):
+        self.ctrl = ctrl
+
+        self.state = 'idle'
+        self.line = -1
+        self.step = 0
+        self.f = None
 
         try:
-            self.sp = serial.Serial(port, baud, timeout = 1)
+            self.sp = serial.Serial(ctrl.args.serial, ctrl.args.baud,
+                                    rtscts = 1, timeout = 0)
 
         except Exception as e:
             log.warning('Failed to open serial port: %s', e)
             return
 
         self.in_buf = ''
-        self.app.input_queue.put('\n')
+        self.out_buf = None
+        self.ctrl.input_queue.put('$echo=0\n\n')
 
-        ioloop.add_handler(self.sp, self.serial_handler, ioloop.READ)
-        ioloop.add_handler(self.app.input_queue._reader.fileno(),
-                           self.queue_handler, ioloop.READ)
+        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)
 
 
     def close(self):
         self.sp.close()
 
 
+    def set_write(self, enable):
+        flags = self.ctrl.ioloop.READ
+        if enable: flags |= self.ctrl.ioloop.WRITE
+        self.ctrl.ioloop.update_handler(self.sp, flags)
+
+
     def serial_handler(self, fd, events):
+        if self.ctrl.ioloop.READ & events: self.serial_read()
+        if self.ctrl.ioloop.WRITE & events: self.serial_write()
+
+
+    def serial_write(self):
+        # Finish writing current line
+        if self.out_buf is not None:
+            try:
+                count = self.sp.write(self.out_buf)
+                log.debug('Wrote %d', count)
+            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
+
+
+    def serial_read(self):
         try:
             data = self.sp.read(self.sp.inWaiting())
             self.in_buf += data.decode('utf-8')
@@ -36,6 +96,7 @@ class AVR():
         except Exception as e:
             log.warning('%s: %s', e, data)
 
+        # Parse incoming serial data into lines
         while True:
             i = self.in_buf.find('\n')
             if i == -1: break
@@ -43,12 +104,41 @@ class AVR():
             self.in_buf = self.in_buf[i + 1:]
 
             if line:
-                self.app.output_queue.put(line)
+                self.ctrl.output_queue.put(line)
                 log.debug(line)
 
 
     def queue_handler(self, fd, events):
-        if self.app.input_queue.empty(): return
+        if self.ctrl.input_queue.empty(): return
 
-        data = self.app.input_queue.get()
+        data = self.ctrl.input_queue.get()
         self.sp.write(data.encode())
+
+
+    def home(self):
+        if self.state != 'idle': raise Exception('Already running')
+        # TODO
+
+
+    def start(self, path):
+        if self.f is None:
+            self.f = open('upload' + path, 'r')
+            self.line = 0
+
+        self.set_write(True)
+        self.state = 'run'
+
+
+    def stop(self):
+        if self.state == 'idle': return
+        self.state == 'stop'
+
+
+    def pause(self, optional):
+        self.state = 'pause'
+
+
+    def step(self):
+        self.step += 1
+        if self.state == 'idle': self.start()
+        else: self.state = 'run'
index 8c5e43e2eebaeca1d7034298bd41609115d1e217..c672acd80ab686581c1e4a72a7044d919478be1b 100644 (file)
@@ -6,19 +6,18 @@ class FileHandler(bbctrl.APIHandler):
     def prepare(self): pass
 
 
-    def delete(self, path):
+    def delete_ok(self, path):
         path = 'upload' + path
         if os.path.exists(path): os.unlink(path)
-        self.write_json('ok')
 
 
-    def put(self, path):
-        path = 'upload' + path
-        if not os.path.exists(path): return
+    def put_ok(self, path):
+        gcode = self.request.files['gcode'][0]
+
+        if not os.path.exists('upload'): os.mkdir('upload')
 
-        with open(path, 'r') as f:
-            for line in f:
-                self.application.input_queue.put(line)
+        with open('upload/' + gcode['filename'], 'wb') as f:
+            f.write(gcode['body'])
 
 
     def get(self, path):
@@ -35,14 +34,3 @@ class FileHandler(bbctrl.APIHandler):
                     files.append(path)
 
         self.write_json(files)
-
-
-    def post(self, path):
-        gcode = self.request.files['gcode'][0]
-
-        if not os.path.exists('upload'): os.mkdir('upload')
-
-        with open('upload/' + gcode['filename'], 'wb') as f:
-            f.write(gcode['body'])
-
-        self.write_json('ok')
index 0bc989fb97a56da25da563657233e0ed6d784b97..ef81a0c7698e76c1dc46549ffcc342332aae9a02 100644 (file)
@@ -4,7 +4,7 @@ from inevent.Constants import *
 
 # Listen for input events
 class Jog(inevent.JogHandler):
-    def __init__(self, args, ioloop):
+    def __init__(self, ctrl):
         config = {
             "deadband": 0.1,
             "axes": [ABS_X, ABS_Y, ABS_RZ, ABS_Z],
@@ -18,7 +18,8 @@ class Jog(inevent.JogHandler):
         self.v = [0.0] * 4
         self.lastV = self.v
 
-        self.processor = inevent.InEvent(ioloop, self, types = "js kbd".split())
+        self.processor = inevent.InEvent(ctrl.ioloop, self,
+                                         types = "js kbd".split())
 
 
     def processed_events(self):
index 9747723545666842f945b7da2911d699b848d13b..4e4ab31b2b22fbef9eb7e383df51f1b108d53a2e 100644 (file)
@@ -3,8 +3,8 @@ import atexit
 
 
 class LCD:
-    def __init__(self, port, addr):
-        self.lcd = lcd.LCD(port, addr)
+    def __init__(self, ctrl):
+        self.lcd = lcd.LCD(ctrl.args.lcd_port, ctrl.args.lcd_addr)
         self.splash()
         atexit.register(self.goodbye)
 
index ea1317742b7b795c2ad7ccea3224316e03551d66..c60ba464e816c601fe4d9e8feb78df7bce1b4000 100644 (file)
@@ -1,7 +1,6 @@
 import os
 import sys
 import json
-import multiprocessing
 import tornado
 import sockjs.tornado
 import logging
@@ -14,34 +13,48 @@ log = logging.getLogger('Web')
 
 
 class LoadHandler(bbctrl.APIHandler):
-    def send_file(self, path):
-        with open(path, 'r') as f:
-            self.write_json(json.load(f))
+    def get(self): self.write_json(self.ctrl.config.load())
 
 
-    def get(self):
-        try:
-            self.send_file('config.json')
-        except Exception as e:
-            log.warning('%s', e)
-            self.send_file(bbctrl.get_resource('default-config.json'))
+class SaveHandler(bbctrl.APIHandler):
+    def put_ok(self): self.ctrl.config.save(self.json)
 
 
+class HomeHandler(bbctrl.APIHandler):
+    def put_ok(self): self.ctrl.avr.home()
+
+
+class StartHandler(bbctrl.APIHandler):
+    def put_ok(self, path): self.ctrl.avr.start(path)
+
+
+class StopHandler(bbctrl.APIHandler):
+    def put_ok(self): self.ctrl.avr.stop()
+
+
+class PauseHandler(bbctrl.APIHandler):
+    def put_ok(self): self.ctrl.avr.pause(False)
 
-class SaveHandler(bbctrl.APIHandler):
-    def post(self):
-        with open('config.json', 'w') as f:
-            json.dump(self.json, f)
 
-        self.application.update_config(self.json)
-        log.info('Saved config')
-        self.write_json('ok')
+class OptionalPauseHandler(bbctrl.APIHandler):
+    def put_ok(self): self.ctrl.avr.pause(True)
 
 
+class StepHandler(bbctrl.APIHandler):
+    def put_ok(self): self.ctrl.avr.step()
+
+
+class OverrideFeedHandler(bbctrl.APIHandler):
+    def put_ok(self, value): self.ctrl.avr.override_feed(float(value))
+
+
+class OverrideSpeedHandler(bbctrl.APIHandler):
+    def put_ok(self, value): self.ctrl.avr.override_speed(float(value))
+
 
 class Connection(sockjs.tornado.SockJSConnection):
     def heartbeat(self):
-        self.timer = self.app.ioloop.call_later(3, self.heartbeat)
+        self.timer = self.ctrl.ioloop.call_later(3, self.heartbeat)
         self.send_json({'heartbeat': self.count})
         self.count += 1
 
@@ -51,117 +64,57 @@ class Connection(sockjs.tornado.SockJSConnection):
 
 
     def on_open(self, info):
-        self.app = self.session.server.app
-        self.timer = self.app.ioloop.call_later(3, self.heartbeat)
+        self.ctrl = self.session.server.ctrl
+
+        self.timer = self.ctrl.ioloop.call_later(3, self.heartbeat)
         self.count = 0;
-        self.app.clients.append(self)
-        self.send_json(self.session.server.app.state)
+
+        self.ctrl.clients.append(self)
+        self.send_json(self.ctrl.state)
 
 
     def on_close(self):
-        self.app.ioloop.remove_timeout(self.timer)
-        self.app.clients.remove(self)
+        self.ctrl.ioloop.remove_timeout(self.timer)
+        self.ctrl.clients.remove(self)
 
 
     def on_message(self, data):
-        self.app.input_queue.put(data + '\n')
+        self.ctrl.input_queue.put(data + '\n')
 
 
 
 class Web(tornado.web.Application):
-    def __init__(self, addr, port, ioloop):
-        # Load config template
-        with open(bbctrl.get_resource('http/config-template.json'), 'r',
-                  encoding = 'utf-8') as f:
-            self.config_template = json.load(f)
-
-        self.ioloop = ioloop
-        self.state = {}
-        self.clients = []
-
-        self.input_queue = multiprocessing.Queue()
-        self.output_queue = multiprocessing.Queue()
-
-        # Handle output queue events
-        ioloop.add_handler(self.output_queue._reader.fileno(),
-                           self.queue_handler, ioloop.READ)
+    def __init__(self, ctrl):
+        self.ctrl = ctrl
 
         handlers = [
             (r'/api/load', LoadHandler),
             (r'/api/save', SaveHandler),
-            (r'/api/file(/.*)?', bbctrl.FileHandler),
+            (r'/api/file(/.+)?', bbctrl.FileHandler),
+            (r'/api/home', HomeHandler),
+            (r'/api/start(/.+)', StartHandler),
+            (r'/api/stop', StopHandler),
+            (r'/api/pause', PauseHandler),
+            (r'/api/pause/optional', OptionalPauseHandler),
+            (r'/api/step', StepHandler),
+            (r'/api/override/feed/([\d.]+)', OverrideFeedHandler),
+            (r'/api/override/speed/([\d.]+)', OverrideSpeedHandler),
             (r'/(.*)', tornado.web.StaticFileHandler,
              {'path': bbctrl.get_resource('http/'),
               "default_filename": "index.html"}),
             ]
 
         router = sockjs.tornado.SockJSRouter(Connection, '/ws')
-        router.app = self
+        router.ctrl = ctrl
 
         tornado.web.Application.__init__(self, router.urls + handlers)
 
         try:
-            self.listen(port, address = addr)
+            self.listen(ctrl.args.port, address = ctrl.args.addr)
 
         except Exception as e:
-            log.error('Failed to bind %s:%d: %s', addr, port, e)
+            log.error('Failed to bind %s:%d: %s', ctrl.args.addr,
+                      ctrl.args.port, e)
             sys.exit(1)
 
-        log.info('Listening on http://%s:%d/', addr, port)
-
-
-    def queue_handler(self, fd, events):
-        try:
-            data = self.output_queue.get()
-            msg = json.loads(data)
-            self.state.update(msg)
-            if self.clients:
-                self.clients[0].broadcast(self.clients, msg)
-
-        except Exception as e:
-            log.error('%s, data: %s', e, data)
-
-
-    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
-
-        cmd = '${}{}={}'.format(index, spec['code'], value)
-        self.input_queue.put(cmd + '\n')
-        #log.info(cmd)
-
-
-    def encode_config_category(self, index, config, category):
-        for key, spec in category.items():
-            if key in config:
-                self.encode_cmd(index, config[key], spec)
-
-
-    def encode_config(self, index, config, tmpl):
-        for category in tmpl.values():
-            self.encode_config_category(index, config, category)
-
-
-    def update_config(self, config):
-        # Motors
-        tmpl = self.config_template['motors']
-        for index in range(len(config['motors'])):
-            self.encode_config(index + 1, config['motors'][index], tmpl)
-
-        # Axes
-        tmpl = self.config_template['axes']
-        axes = 'xyzabc'
-        for axis in axes:
-            if not axis in config['axes']: continue
-            self.encode_config(axis, config['axes'][axis], tmpl)
-
-        # Switches
-        tmpl = self.config_template['switches']
-        for index in range(len(config['switches'])):
-            self.encode_config_category(index + 1,
-                                        config['switches'][index], tmpl)
-
-        # Spindle
-        tmpl = self.config_template['spindle']
-        self.encode_config_category('', config['spindle'], tmpl)
+        log.info('Listening on http://%s:%d/', ctrl.args.addr, ctrl.args.port)
index 43d4b6f812bc7a2748149820daeb53a5b66f46b2..5418cd5d7693cdd46a70130983e5d0a7ee9b1c82 100755 (executable)
@@ -11,10 +11,12 @@ from pkg_resources import Requirement, resource_filename
 
 from bbctrl.APIHandler import APIHandler
 from bbctrl.FileHandler import FileHandler
+from bbctrl.Config import Config
 from bbctrl.LCD import LCD
 from bbctrl.AVR import AVR
 from bbctrl.Web import Web
 from bbctrl.Jog import Jog
+from bbctrl.Ctrl import Ctrl
 
 
 def get_resource(path):
@@ -64,17 +66,8 @@ def run():
     # Create ioloop
     ioloop = tornado.ioloop.IOLoop.current()
 
-    # Start the web server
-    app = Web(args.addr, args.port, ioloop)
-
-    # Start AVR driver
-    avr = AVR(args.serial, args.baud, ioloop, app)
-
-    # Start job input controler
-    jog = Jog(args, ioloop)
-
-    # Start LCD driver
-    lcd = LCD(args.lcd_port, args.lcd_addr)
+    # Start controller
+    ctrl = Ctrl(args, ioloop)
 
     try:
         ioloop.start()