Complete reorg, eliminated threads
authorJoseph Coffland <joseph@cauldrondevelopment.com>
Wed, 22 Jun 2016 07:07:36 +0000 (00:07 -0700)
committerJoseph Coffland <joseph@cauldrondevelopment.com>
Wed, 22 Jun 2016 07:07:36 +0000 (00:07 -0700)
12 files changed:
Makefile
scripts/bbctrl.init.d
src/py/bbctrl/AVR.py [new file with mode: 0644]
src/py/bbctrl/Jog.py [new file with mode: 0644]
src/py/bbctrl/LCD.py [new file with mode: 0644]
src/py/bbctrl/Web.py [new file with mode: 0644]
src/py/bbctrl/__init__.py
src/py/bbctrl/default-config.json [new file with mode: 0644]
src/py/inevent/FindDevices.py
src/py/inevent/InEvent.py
src/py/inevent/JogHandler.py
src/py/lcd/__init__.py

index 593a92023e01e969d0d0e0721f6a444d0be94f34..2c6709b6d60b1d88815ca51964af9a1c45f45c3b 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -17,8 +17,12 @@ STATIC    := $(shell find src/resources -type f)
 STATIC    := $(patsubst src/resources/%,$(TARGET)/%,$(STATIC))
 TEMPLS    := $(wildcard src/jade/templates/*.jade)
 
+RSYNC_EXCLUDE := \*.pyc __pycache__ \*.egg-info \\\#* \*~ .\\\#\*
+RSYNC_EXCLUDE := $(patsubst %,--exclude %,$(RSYNC_EXCLUDE))
+RSYNC_OPTS := $(RSYNC_EXCLUDE) -rLv --no-g
+
 ifndef DEST
-DEST=mnt/
+DEST=mnt
 endif
 
 WATCH := src/jade src/jade/templates src/stylus src/js src/resources Makefile
@@ -27,10 +31,8 @@ all: html css js static
 
 copy: all
        mkdir -p $(DEST)/bbctrl/src/py $(DEST)/bbctrl/build
-       rsync -rLv --no-g --exclude \*.pyc --exclude __pycache__ \
-         --exclude \*.egg-info src/py $(DEST)/bbctrl/src/
-       rsync -av --no-g build/http $(DEST)/bbctrl/build
-       rsync -av --no-g setup.py README.md $(DEST)/bbctrl
+       rsync $(RSYNC_OPTS) src/py $(DEST)/bbctrl/src/
+       rsync $(RSYNC_OPTS) setup.py README.md $(DEST)/bbctrl
 
 mount:
        mkdir -p $(DEST)
index 01f356af659960b68c6e94a5de2caeed2d148adc..54d4366a2091ad84e3afdda8390a7908a1786f27 100755 (executable)
 # Description:       Buildbotics Controller Web service
 ### END INIT INFO
 
-DAEMON=/usr/local/bin/bbctrl
+
 DAEMON_NAME=bbctrl
+DAEMON=/usr/local/bin/$DAEMON_NAME
 DAEMON_OPTS=""
 DAEMON_USER=root
 DAEMON_DIR=/var/lib/$DAEMON_NAME
 PIDFILE=/var/run/$DAEMON_NAME.pid
+DEFAULTS=/etc/default/$DAEMON_NAME
+
 
 . /lib/lsb/init-functions
 
 
+if [ -e $DEFAULTS ]; then
+    . $DEFAULTS
+fi
+
+
 do_start () {
     log_daemon_msg "Starting system $DAEMON_NAME daemon"
     mkdir -p $DAEMON_DIR &&
     start-stop-daemon --start --background --pidfile $PIDFILE --make-pidfile \
         --user $DAEMON_USER --chuid $DAEMON_USER --chdir $DAEMON_DIR \
-        --startas /bin/bash -- \
-        -c "exec $DAEMON $DAEMON_OPTS > /var/log/$DAEMON_NAME.log 2>&1"
+        --startas $DAEMON -- $DAEMON_OPTS -l /var/log/$DAEMON_NAME.log
     log_end_msg $?
 }
 
diff --git a/src/py/bbctrl/AVR.py b/src/py/bbctrl/AVR.py
new file mode 100644 (file)
index 0000000..df5e395
--- /dev/null
@@ -0,0 +1,54 @@
+import serial
+import logging
+
+
+log = logging.getLogger('AVR')
+
+
+class AVR():
+    def __init__(self, port, baud, ioloop, app):
+        self.app = app
+
+        try:
+            self.sp = serial.Serial(port, baud, timeout = 1)
+
+        except Exception as e:
+            log.warning('Failed to open serial port: %s', e)
+            return
+
+        self.in_buf = ''
+        self.app.input_queue.put('\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)
+
+
+    def close(self):
+        self.sp.close()
+
+
+    def serial_handler(self, fd, events):
+        try:
+            data = self.sp.read(self.sp.inWaiting())
+            self.in_buf += data.decode('utf-8')
+
+        except Exception as e:
+            log.warning('%s: %s', e, data)
+
+        while True:
+            i = self.in_buf.find('\n')
+            if i == -1: break
+            line = self.in_buf[0:i].strip()
+            self.in_buf = self.in_buf[i + 1:]
+
+            if line:
+                self.app.output_queue.put(line)
+                log.debug(line)
+
+
+    def queue_handler(self, fd, events):
+        if self.app.input_queue.empty(): return
+
+        data = self.app.input_queue.get()
+        self.sp.write(data.encode())
diff --git a/src/py/bbctrl/Jog.py b/src/py/bbctrl/Jog.py
new file mode 100644 (file)
index 0000000..0bc989f
--- /dev/null
@@ -0,0 +1,39 @@
+import inevent
+from inevent.Constants import *
+
+
+# Listen for input events
+class Jog(inevent.JogHandler):
+    def __init__(self, args, ioloop):
+        config = {
+            "deadband": 0.1,
+            "axes": [ABS_X, ABS_Y, ABS_RZ, ABS_Z],
+            "arrows": [ABS_HAT0X, ABS_HAT0Y],
+            "speed": [0x120, 0x121, 0x122, 0x123],
+            "activate": [0x124, 0x126, 0x125, 0x127],
+            }
+
+        super().__init__(config)
+
+        self.v = [0.0] * 4
+        self.lastV = self.v
+
+        self.processor = inevent.InEvent(ioloop, self, types = "js kbd".split())
+
+
+    def processed_events(self):
+        if self.v != self.lastV:
+            self.lastV = self.v
+
+            v = ["{:6.5f}".format(x) for x in self.v]
+            cmd = '$jog ' + ' '.join(v) + '\n'
+            input_queue.put(cmd)
+
+
+    def changed(self):
+        if self.speed == 1: scale = 1.0 / 128.0
+        if self.speed == 2: scale = 1.0 / 32.0
+        if self.speed == 3: scale = 1.0 / 4.0
+        if self.speed == 4: scale = 1.0
+
+        self.v = [x * scale for x in self.axes]
diff --git a/src/py/bbctrl/LCD.py b/src/py/bbctrl/LCD.py
new file mode 100644 (file)
index 0000000..9747723
--- /dev/null
@@ -0,0 +1,21 @@
+import lcd
+import atexit
+
+
+class LCD:
+    def __init__(self, port, addr):
+        self.lcd = lcd.LCD(port, addr)
+        self.splash()
+        atexit.register(self.goodbye)
+
+
+    def splash(self):
+        self.lcd.clear()
+        self.lcd.display(0, 'Buildbotics', lcd.JUSTIFY_CENTER)
+        self.lcd.display(1, 'Controller', lcd.JUSTIFY_CENTER)
+        self.lcd.display(3, '*Ready*', lcd.JUSTIFY_CENTER)
+
+
+    def goodbye(self):
+        self.lcd.clear()
+        self.lcd.display(1, 'Goodbye', lcd.JUSTIFY_CENTER)
diff --git a/src/py/bbctrl/Web.py b/src/py/bbctrl/Web.py
new file mode 100644 (file)
index 0000000..3ac8ece
--- /dev/null
@@ -0,0 +1,227 @@
+import os
+import sys
+import json
+import multiprocessing
+import tornado
+import sockjs.tornado
+import logging
+
+import bbctrl
+
+
+log = logging.getLogger('Web')
+
+
+class APIHandler(tornado.web.RequestHandler):
+    def prepare(self):
+        self.json = {}
+
+        if self.request.body:
+            try:
+                self.json = tornado.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:
+            log.warning('%s', e)
+            self.send_file(bbctrl.get_resource('default-config.json'))
+
+
+
+class SaveHandler(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 FileHandler(APIHandler):
+    def prepare(self): pass
+
+
+    def delete(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
+
+        with open(path, 'r') as f:
+            for line in f:
+                self.application.input_queue.put(line)
+
+
+    def get(self, path):
+        if path:
+            with open('upload/' + path, 'r') as f:
+                self.write_json(f.read())
+            return
+
+        files = []
+
+        if os.path.exists('upload'):
+            for path in os.listdir('upload'):
+                if os.path.isfile('upload/' + path):
+                    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')
+
+
+
+class Connection(sockjs.tornado.SockJSConnection):
+    def on_open(self, info):
+        self.session.server.app.clients.append(self)
+        self.send(str.encode(json.dumps(self.session.server.app.state)))
+
+
+    def on_close(self):
+        self.session.server.app.clients.remove(self)
+
+
+    def on_message(self, data):
+        self.session.server.app.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.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)
+
+        handlers = [
+            (r'/api/load', LoadHandler),
+            (r'/api/save', SaveHandler),
+            (r'/api/file(/.*)?', FileHandler),
+            (r'/(.*)', tornado.web.StaticFileHandler,
+             {'path': bbctrl.get_resource('http/'),
+              "default_filename": "index.html"}),
+            ]
+
+        router = sockjs.tornado.SockJSRouter(Connection, '/ws')
+        router.app = self
+
+        tornado.web.Application.__init__(self, router.urls + handlers)
+
+        try:
+            self.listen(port, address = addr)
+
+        except Exception as e:
+            log.error('Failed to bind %s:%d: %s', addr, 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)
index ba0e6becbb98bbd16796894df4c4153818224b48..ccf09dcbc836459cc97f2057edf205b4e1dcc291 100755 (executable)
 import os
 import sys
 import signal
-from tornado import web, ioloop, escape
-from sockjs.tornado import SockJSRouter, SockJSConnection
-import json
-import serial
-import multiprocessing
-import time
-import select
-import atexit
+import tornado
 import argparse
+import logging
 
 from pkg_resources import Requirement, resource_filename
 
-import lcd
-import inevent
-from inevent.Constants import *
-
-
-DIR = os.path.dirname(__file__)
-
-config = {
-    "deadband": 0.1,
-    "axes": [ABS_X, ABS_Y, ABS_RZ, ABS_Z],
-    "arrows": [ABS_HAT0X, ABS_HAT0Y],
-    "speed": [0x120, 0x121, 0x122, 0x123],
-    "activate": [0x124, 0x126, 0x125, 0x127],
-    "verbose": False
-    }
-
-state = {}
-clients = []
-
-input_queue = multiprocessing.Queue()
-output_queue = multiprocessing.Queue()
+from bbctrl.LCD import LCD
+from bbctrl.AVR import AVR
+from bbctrl.Web import Web
+from bbctrl.Jog import Jog
 
 
 def get_resource(path):
     return resource_filename(Requirement.parse('bbctrl'), 'bbctrl/' + path)
 
 
-def on_exit(sig, func = None):
-    print('exit handler triggered')
+def on_exit(sig = 0, func = None):
+    logging.info('Exit handler triggered: signal = %d', sig)
     sys.exit(1)
 
 
-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(get_resource('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 FileHandler(APIHandler):
-    def prepare(self): pass
-
-
-    def delete(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
-
-        with open(path, 'r') as f:
-            for line in f:
-                input_queue.put(line)
-
-
-    def get(self, path):
-        if path:
-            with open('upload/' + path, 'r') as f:
-                self.write_json(f.read())
-            return
-
-        files = []
-
-        if os.path.exists('upload'):
-            for path in os.listdir('upload'):
-                if os.path.isfile('upload/' + path):
-                    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')
-
-
-class SerialProcess(multiprocessing.Process):
-    def __init__(self, port, baud, input_queue, output_queue):
-        multiprocessing.Process.__init__(self)
-        self.input_queue = input_queue
-        self.output_queue = output_queue
-        self.sp = serial.Serial(port, baud, timeout = 1)
-        self.input_queue.put('\n')
-
-
-    def close(self):
-        self.sp.close()
-
-
-    def writeSerial(self, data):
-        self.sp.write(data.encode())
-
-
-    def readSerial(self):
-        return self.sp.readline().replace(b"\n", b"")
-
-
-    def run(self):
-        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()
-
-                # send it to the serial device
-                self.writeSerial(data)
-
-            # look for incoming serial data
-            if self.sp.inWaiting() > 0:
-                try:
-                    data = self.readSerial()
-                    data = data.decode('utf-8').strip()
-                    if not data: continue
-
-                    # send it back to tornado
-                    self.output_queue.put(data)
-
-                    print(data)
-
-                except Exception as e:
-                    print(e, data)
-
-
-
-class Connection(SockJSConnection):
-    def on_open(self, info):
-        clients.append(self)
-        self.send(str.encode(json.dumps(state)))
-
-
-    def on_close(self):
-        clients.remove(self)
-
-
-    def on_message(self, data):
-        input_queue.put(data + '\n')
-
-
-
-# check the queue for pending messages, and relay them to all connected clients
-def checkQueue():
-    while not output_queue.empty():
-        try:
-            data = output_queue.get()
-            msg = json.loads(data)
-            state.update(msg)
-            if clients: clients[0].broadcast(clients, msg)
-
-        except Exception as e:
-            print('ERROR: {}, data: {}'.format(e, data))
-
-
-handlers = [
-    (r'/api/load', LoadHandler),
-    (r'/api/save', SaveHandler),
-    (r'/api/file(/.*)?', FileHandler),
-    (r'/(.*)', web.StaticFileHandler,
-     {'path': os.path.join(DIR, get_resource('http/')),
-      "default_filename": "index.html"}),
-    ]
-
-router = SockJSRouter(Connection, '/ws')
-
-
-# Listen for input events
-class JogHandler(inevent.JogHandler):
-    def __init__(self, config):
-        super().__init__(config)
-
-        self.v = [0.0] * 4
-        self.lastV = self.v
-
-
-    def processed_events(self):
-        if self.v != self.lastV:
-            self.lastV = self.v
-
-            v = ["{:6.5f}".format(x) for x in self.v]
-            cmd = '$jog ' + ' '.join(v) + '\n'
-            input_queue.put(cmd)
-
-
-    def changed(self):
-        if self.speed == 1: scale = 1.0 / 128.0
-        if self.speed == 2: scale = 1.0 / 32.0
-        if self.speed == 3: scale = 1.0 / 4.0
-        if self.speed == 4: scale = 1.0
-
-        self.v = [x * scale for x in self.axes]
-
-
-
-def checkEvents():
-    eventProcessor.process_events(eventHandler)
-    eventHandler.processed_events()
-
-eventProcessor = inevent.InEvent(types = "js kbd".split())
-eventHandler = JogHandler(config)
-
-
-def splash(screen):
-    screen.clear()
-    screen.display(0, 'Buildbotics', lcd.JUSTIFY_CENTER)
-    screen.display(1, 'Controller', lcd.JUSTIFY_CENTER)
-    screen.display(3, '*Ready*', lcd.JUSTIFY_CENTER)
-
-
-def goodbye(screen):
-    screen.clear()
-    screen.display(1, 'Goodbye', lcd.JUSTIFY_CENTER)
-
-
 def parse_args():
     parser = argparse.ArgumentParser(
         description = 'Buildbotics Machine Controller')
@@ -346,57 +42,45 @@ def parse_args():
                         help = 'LCD I2C address')
     parser.add_argument('-v', '--verbose', action = 'store_true',
                         help = 'Verbose output')
+    parser.add_argument('-l', '--log', metavar = "FILE",
+                        help = 'Set a log file')
 
     return parser.parse_args()
 
 
 def run():
-    # Set signal handler
-    signal.signal(signal.SIGTERM, on_exit)
-
-    global args
     args = parse_args()
 
-    # Load config template
-    global config_template
-    with open(get_resource('http/config-template.json'), 'r',
-              encoding = 'utf-8') as f:
-        config_template = json.load(f)
-
     # Init logging
-    import logging
-    logging.getLogger().setLevel(logging.DEBUG)
+    log = logging.getLogger()
+    log.setLevel(logging.DEBUG if args.verbose else logging.INFO)
+    if args.log: log.addHandler(logging.FileHandler(args.log, mode = 'w'))
 
-    # Start the serial worker
-    try:
-        sp = SerialProcess(args.serial, args.baud, input_queue, output_queue)
-        sp.daemon = True
-        sp.start()
-    except Exception as e:
-        print('Failed to open serial port:', e)
-
-    # Adjust the interval according to frames sent by serial port
-    ioloop.PeriodicCallback(checkQueue, 100).start()
-    ioloop.PeriodicCallback(checkEvents, 100).start()
+    # Set signal handler
+    signal.signal(signal.SIGTERM, on_exit)
 
-    # Setup LCD
-    global screen
-    screen = lcd.LCD(args.lcd_port, args.lcd_addr)
-    splash(screen)
-    atexit.register(goodbye, screen)
+    # Create ioloop
+    ioloop = tornado.ioloop.IOLoop.current()
 
     # Start the web server
-    app = web.Application(router.urls + handlers)
+    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)
 
     try:
-        app.listen(args.port, address = args.addr)
-    except Exception as e:
-        print('Failed to bind {}:{}:'.format(args.addr, args.port), e)
-        sys.exit(1)
+        ioloop.start()
 
-    print('Listening on http://{}:{}/'.format(args.addr, args.port))
+    except KeyboardInterrupt:
+        on_exit()
 
-    ioloop.IOLoop.instance().start()
+    except: log.exception('')
 
 
 if __name__ == "__main__": run()
diff --git a/src/py/bbctrl/default-config.json b/src/py/bbctrl/default-config.json
new file mode 100644 (file)
index 0000000..5175343
--- /dev/null
@@ -0,0 +1,75 @@
+{
+  "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 17a667aaf81a6ac692f00481d3377fb04178e644..bfcbd4e5080807cfe27b4ad7107b91c06a4c0b7f 100644 (file)
 # SOFTWARE.
 
 import re
+import logging
 from inevent.Constants import *
 
+log = logging.getLogger('inevent')
+
 
 def test_bit(nlst, b):
   index = b / 32
@@ -50,7 +53,7 @@ def EvToStr(events):
   if test_bit(events, EV_FF):        s.append("EV_FF" )
   if test_bit(events, EV_PWR):       s.append("EV_PWR")
   if test_bit(events, EV_FF_STATUS): s.append("EV_FF_STATUS")
-    
+
   return s
 
 
@@ -68,13 +71,13 @@ class DeviceCapabilities(object):
     self.EV_PWRevents = []
     self.EV_FF_STATUSevents = []
     self.eventTypes = []
-    
+
     match = re.search(".*Bus=([0-9A-Fa-f]+).*Vendor=([0-9A-Fa-f]+).*"
                       "Product=([0-9A-Fa-f]+).*Version=([0-9A-Fa-f]+).*",
                       firstLine)
 
     if not match:
-      print("Do not understand device ID:", line)
+      log.warning("Do not understand device ID:", line)
       self.bus = 0
       self.vendor = 0
       self.product = 0
@@ -182,7 +185,7 @@ class DeviceCapabilities(object):
         self.sysfs, self.uniq, self.handlers, self.eventIndex, self.isKeyboard,
         self.isMouse, self.isJoystick, EvToStr(self.eventTypes)))
 
-        
+
 deviceCapabilities = []
 
 
@@ -193,7 +196,7 @@ def get_devices(filename = "/proc/bus/input/devices"):
     for line in filehandle:
       if line[0] == "I":
         deviceCapabilities.append(DeviceCapabilities(line, filehandle))
-              
+
   return deviceCapabilities
 
 
index fab068aff1a393e6cc72f949e3c7017329b022e3..c98ae2400e2726b8ebd3d13f7ddd44ca348dc52b 100644 (file)
@@ -30,6 +30,8 @@ import pyudev
 import re
 import select
 import errno
+import functools
+import logging
 
 from inevent.EventHandler import EventHandler
 from inevent import Keys
@@ -37,6 +39,8 @@ from inevent.Constants import *
 from inevent.EventStream import EventStream
 
 
+log = logging.getLogger('inevent')
+
 _KEYS = (k for k in vars(Keys) if not k.startswith('_'))
 KEY_CODE = dict((k, getattr(Keys, k)) for k in _KEYS)
 CODE_KEY = {}
@@ -113,18 +117,22 @@ class InEvent(object):
   The keys are listed in inevent.Constants.py or /usr/include/linux/input.h
   Note that the key names refer to a US keyboard.
   """
-  def __init__(self, types = ["kbd", "mouse", "js"]):
+  def __init__(self, ioloop, cb, types = ["kbd", "mouse", "js"]):
+    self.ioloop = ioloop
+    self.cb = cb
     self.streams = []
     self.handler = EventHandler()
     self.types = types
 
     devs = list(find_devices(types))
-    for index, type in devs: self.add_stream(index, type)
+    for index, type in devs:
+      self.add_stream(index, type)
 
     self.udevCtx = pyudev.Context()
     self.udevMon = pyudev.Monitor.from_netlink(self.udevCtx)
     self.udevMon.filter_by(subsystem = 'input')
     self.udevMon.start()
+    ioloop.add_handler(self.udevMon.fileno(), self.udev_handler, ioloop.READ)
 
 
   def process_udev_event(self):
@@ -145,52 +153,42 @@ class InEvent(object):
       self.remove_stream(devIndex)
 
 
-  def process_events(self, cb = None):
-    """
-    Handle all events that have been triggered since the last call.
-    """
-
-    # Gather list of file descriptors to watch
-    selectlist = [x.filehandle for x in self.streams]
-    udevFD = self.udevMon.fileno()
-    selectlist.append(udevFD)
-
-    processedEvent = True
-    while processedEvent:
-      processedEvent = False
-
-      # Select
-      ready = select.select(selectlist, [], [], 0)[0]
+  def stream_handler(self, fd, events):
+    for stream in self.streams:
+      if stream.filehandle == fd:
+        while True:
+          event = stream.read()
+          if event: self.handler.event(event, self.cb)
+          else: break
 
-      # Handle events
-      for fd in ready:
-        if fd == udevFD:
-          self.process_udev_event()
-          processedEvent = True
 
-        for stream in self.streams:
-          if stream.filehandle == fd:
-            event = stream.read()
-            if event:
-              self.handler.event(event, cb)
-              processedEvent = True
+  def udev_handler(self, fd, events):
+    self.process_udev_event()
 
 
   def add_stream(self, devIndex, devType):
     try:
-      self.streams.append(EventStream(devIndex, devType))
-      print('Added {}[{:d}]'.format(devType, devIndex))
+      stream = EventStream(devIndex, devType)
+      self.streams.append(stream)
+
+      self.ioloop.add_handler(stream.filehandle, self.stream_handler,
+                              self.ioloop.READ)
+
+      log.info('Added %s[%d]', devType, devIndex)
 
     except OSError as e:
-      if not e.errno in [errno.EPERM, errno.EACCES]: raise e
+      if e.errno in [errno.EPERM, errno.EACCES]:
+        log.warning('Failed to add %s[%d]: %s', devType, devIndex, e)
+      else: raise e
 
 
   def remove_stream(self, devIndex):
     for stream in self.streams:
       if stream.devIndex == devIndex:
         self.streams.remove(stream)
-        print('Removed {}[{:d}]'.format(stream.devType, devIndex))
-        break
+        self.ioloop.remove_handler(stream.filehandle)
+
+        log.info('Removed %s[%d]', stream.devType, devIndex)
 
 
   def key_state(self, key):
@@ -249,4 +247,3 @@ class InEvent(object):
     Only do this when you're finished with this object. You can't use it again.
     """
     for s in self.streams: s.release()
-
index f14b55d58fd6f75d93613ef0b245bbcc963832aa..561a22f9a3772109952c7c4c897cae0b4dd14a25 100644 (file)
@@ -1,25 +1,32 @@
+import logging
+
 from inevent.Constants import *
 
 
+log = logging.getLogger('inevent')
+
+
 def axes_to_string(axes):
-    return "({:6.3f}, {:6.3f}, {:6.3f}, {:6.3f})".format(*axes)
+    return '({:6.3f}, {:6.3f}, {:6.3f})'.format(*axes)
 
 
-def print_event(event, state):
-    print("{} {}: ".format(event.get_source(), event.get_type_name()), end = '')
+def event_to_string(event, state):
+    s = '{} {}: '.format(event.get_source(), event.get_type_name())
 
     if event.type == EV_ABS:
-        print(axes_to_string(state.get_joystick3d()) + " " +
-              axes_to_string(state.get_joystickR3d()) + " " +
-              "({:2.0f}, {:2.0f}) ".format(*state.get_hat()))
+        s += axes_to_string(state.get_joystick3d()) + ' ' + \
+            axes_to_string(state.get_joystickR3d()) + ' ' + \
+            '({:2.0f}, {:2.0f}) '.format(*state.get_hat())
 
     if event.type == EV_REL:
-        print("({:d}, {:d}) ".format(*state.get_mouse()) +
-              "({:d}, {:d})".format(*state.get_wheel()))
+        s += '({:d}, {:d}) '.format(*state.get_mouse()) + \
+            '({:d}, {:d})'.format(*state.get_wheel())
 
     if event.type == EV_KEY:
-        state = "pressed" if event.value else "released"
-        print("0x{:x} {}".format(event.code, state))
+        state = 'pressed' if event.value else 'released'
+        s += '0x{:x} {}'.format(event.code, state)
+
+    return s
 
 
 class JogHandler:
@@ -31,7 +38,7 @@ class JogHandler:
 
 
     def changed(self):
-        print(axes_to_string(self.axes) + " x {:d}".format(self.speed))
+        log.debug(axes_to_string(self.axes) + ' x {:d}'.format(self.speed))
 
 
     def __call__(self, event, state):
@@ -47,12 +54,12 @@ class JogHandler:
             axis = self.config['arrows'].index(event.code)
 
             if event.value < 0:
-                if axis == 1: print('up')
-                else: print('left')
+                if axis == 1: log.debug('up')
+                else: log.debug('left')
 
             elif 0 < event.value:
-                if axis == 1: print('down')
-                else: print('right')
+                if axis == 1: log.debug('down')
+                else: log.debug('right')
 
         elif event.type == EV_KEY and event.code in self.config['speed']:
             old_speed = self.speed
@@ -65,7 +72,7 @@ class JogHandler:
             if event.value: self.activate |= 1 << index
             else: self.activate &= ~(1 << index)
 
-        if self.config.get('verbose', False): print_event(event, state)
+        log.debug(event_to_string(event, state))
 
         # Update axes
         old_axes = list(self.axes)
index 0380705a5495c0d70296a232095ee50619723c50..b14f3174bfca29895f9b04140897c380799cf801 100755 (executable)
@@ -6,6 +6,10 @@ except:
     import smbus2 as smbus
 
 import time
+import logging
+
+
+log = logging.getLogger('LCD')
 
 
 # Control flags
@@ -68,7 +72,7 @@ class LCD:
             self.bus = smbus.SMBus(port)
         except FileNotFoundError as e:
             self.bus = None
-            print('Failed to open LCD device:', e)
+            log.warning('Failed to open device: %s', e)
 
         self.backlight = True