From 4d07b1fa71a67bddd69ce2b5367c8877b54d0de3 Mon Sep 17 00:00:00 2001 From: Joseph Coffland Date: Wed, 22 Jun 2016 00:07:36 -0700 Subject: [PATCH] Complete reorg, eliminated threads --- Makefile | 12 +- scripts/bbctrl.init.d | 13 +- src/py/bbctrl/AVR.py | 54 +++++ src/py/bbctrl/Jog.py | 39 +++ src/py/bbctrl/LCD.py | 21 ++ src/py/bbctrl/Web.py | 227 ++++++++++++++++++ src/py/bbctrl/__init__.py | 378 +++--------------------------- src/py/bbctrl/default-config.json | 75 ++++++ src/py/inevent/FindDevices.py | 13 +- src/py/inevent/InEvent.py | 67 +++--- src/py/inevent/JogHandler.py | 39 +-- src/py/lcd/__init__.py | 6 +- 12 files changed, 532 insertions(+), 412 deletions(-) create mode 100644 src/py/bbctrl/AVR.py create mode 100644 src/py/bbctrl/Jog.py create mode 100644 src/py/bbctrl/LCD.py create mode 100644 src/py/bbctrl/Web.py create mode 100644 src/py/bbctrl/default-config.json diff --git a/Makefile b/Makefile index 593a920..2c6709b 100644 --- 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) diff --git a/scripts/bbctrl.init.d b/scripts/bbctrl.init.d index 01f356a..54d4366 100755 --- a/scripts/bbctrl.init.d +++ b/scripts/bbctrl.init.d @@ -10,23 +10,30 @@ # 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 index 0000000..df5e395 --- /dev/null +++ b/src/py/bbctrl/AVR.py @@ -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 index 0000000..0bc989f --- /dev/null +++ b/src/py/bbctrl/Jog.py @@ -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 index 0000000..9747723 --- /dev/null +++ b/src/py/bbctrl/LCD.py @@ -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 index 0000000..3ac8ece --- /dev/null +++ b/src/py/bbctrl/Web.py @@ -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) diff --git a/src/py/bbctrl/__init__.py b/src/py/bbctrl/__init__.py index ba0e6be..ccf09dc 100755 --- a/src/py/bbctrl/__init__.py +++ b/src/py/bbctrl/__init__.py @@ -3,331 +3,27 @@ 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 index 0000000..5175343 --- /dev/null +++ b/src/py/bbctrl/default-config.json @@ -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": { + } +} diff --git a/src/py/inevent/FindDevices.py b/src/py/inevent/FindDevices.py index 17a667a..bfcbd4e 100644 --- a/src/py/inevent/FindDevices.py +++ b/src/py/inevent/FindDevices.py @@ -27,8 +27,11 @@ # 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 diff --git a/src/py/inevent/InEvent.py b/src/py/inevent/InEvent.py index fab068a..c98ae24 100644 --- a/src/py/inevent/InEvent.py +++ b/src/py/inevent/InEvent.py @@ -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() - diff --git a/src/py/inevent/JogHandler.py b/src/py/inevent/JogHandler.py index f14b55d..561a22f 100644 --- a/src/py/inevent/JogHandler.py +++ b/src/py/inevent/JogHandler.py @@ -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) diff --git a/src/py/lcd/__init__.py b/src/py/lcd/__init__.py index 0380705..b14f317 100755 --- a/src/py/lcd/__init__.py +++ b/src/py/lcd/__init__.py @@ -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 -- 2.27.0