From 0d9a31ec5bc4b44a64d5afc710bb0702e031c262 Mon Sep 17 00:00:00 2001 From: Joseph Coffland Date: Sat, 25 Jun 2016 15:57:54 -0700 Subject: [PATCH] Basic GCode playback --- src/jade/templates/control-view.jade | 8 +- src/js/api.js | 2 +- src/js/app.js | 2 +- src/js/control-view.js | 46 +++++--- src/py/bbctrl/APIHandler.py | 22 ++++ src/py/bbctrl/AVR.py | 110 ++++++++++++++++-- src/py/bbctrl/FileHandler.py | 26 ++--- src/py/bbctrl/Jog.py | 5 +- src/py/bbctrl/LCD.py | 4 +- src/py/bbctrl/Web.py | 159 ++++++++++----------------- src/py/bbctrl/__init__.py | 15 +-- 11 files changed, 230 insertions(+), 169 deletions(-) diff --git a/src/jade/templates/control-view.jade b/src/jade/templates/control-view.jade index 05b43fe..0bee1d6 100644 --- a/src/jade/templates/control-view.jade +++ b/src/jade/templates/control-view.jade @@ -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", diff --git a/src/js/api.js b/src/js/api.js index 8b26cd7..ce8424a 100644 --- a/src/js/api.js +++ b/src/js/api.js @@ -53,7 +53,7 @@ module.exports = { data: data }, config); - return api_cb('POST', url, undefined, config); + return api_cb('PUT', url, undefined, config); }, diff --git a/src/js/app.js b/src/js/app.js index 3de4755..16d4481 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -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); diff --git a/src/js/control-view.js b/src/js/control-view.js index 6d7a160..1fd182e 100644 --- a/src/js/control-view.js +++ b/src/js/control-view.js @@ -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'); } }, diff --git a/src/py/bbctrl/APIHandler.py b/src/py/bbctrl/APIHandler.py index dcfe7a1..6317c17 100644 --- a/src/py/bbctrl/APIHandler.py +++ b/src/py/bbctrl/APIHandler.py @@ -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 = {} diff --git a/src/py/bbctrl/AVR.py b/src/py/bbctrl/AVR.py index df5e395..21b3b3a 100644 --- a/src/py/bbctrl/AVR.py +++ b/src/py/bbctrl/AVR.py @@ -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' diff --git a/src/py/bbctrl/FileHandler.py b/src/py/bbctrl/FileHandler.py index 8c5e43e..c672acd 100644 --- a/src/py/bbctrl/FileHandler.py +++ b/src/py/bbctrl/FileHandler.py @@ -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') diff --git a/src/py/bbctrl/Jog.py b/src/py/bbctrl/Jog.py index 0bc989f..ef81a0c 100644 --- a/src/py/bbctrl/Jog.py +++ b/src/py/bbctrl/Jog.py @@ -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): diff --git a/src/py/bbctrl/LCD.py b/src/py/bbctrl/LCD.py index 9747723..4e4ab31 100644 --- a/src/py/bbctrl/LCD.py +++ b/src/py/bbctrl/LCD.py @@ -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) diff --git a/src/py/bbctrl/Web.py b/src/py/bbctrl/Web.py index ea13177..c60ba46 100644 --- a/src/py/bbctrl/Web.py +++ b/src/py/bbctrl/Web.py @@ -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) diff --git a/src/py/bbctrl/__init__.py b/src/py/bbctrl/__init__.py index 43d4b6f..5418cd5 100755 --- a/src/py/bbctrl/__init__.py +++ b/src/py/bbctrl/__init__.py @@ -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() -- 2.27.0