From: Joseph Coffland Date: Wed, 22 Jun 2016 00:35:31 +0000 (-0700) Subject: Packaged Python code with setuptools X-Git-Url: https://git.buildbotics.com/?a=commitdiff_plain;h=e68acf1f316d10147170f68bec53980816d476ac;p=bbctrl-firmware Packaged Python code with setuptools --- diff --git a/.gitignore b/.gitignore index 70a8c44..150b38a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,10 @@ .sconf_temp/ .sconsign.dblite -/http /build node_modules *~ \#* *.pyc __pycache__ +*.egg-info +/upload diff --git a/Makefile b/Makefile index 658cd52..87e7417 100644 --- a/Makefile +++ b/Makefile @@ -6,18 +6,19 @@ STYLUS := $(NODE_MODS)/stylus/bin/stylus AP := $(NODE_MODS)/autoprefixer/autoprefixer BROWSERIFY := $(NODE_MODS)/browserify/bin/cmd.js +TARGET := build/http HTML := index -HTML := $(patsubst %,http/%.html,$(HTML)) +HTML := $(patsubst %,$(TARGET)/%.html,$(HTML)) CSS := $(wildcard src/stylus/*.styl) CSS_ASSETS := build/css/style.css JS := $(wildcard src/js/*.js) -JS_ASSETS := http/js/assets.js +JS_ASSETS := $(TARGET)/js/assets.js STATIC := $(shell find src/resources -type f) -STATIC := $(patsubst src/resources/%,http/%,$(STATIC)) +STATIC := $(patsubst src/resources/%,$(TARGET)/%,$(STATIC)) TEMPLS := $(wildcard src/jade/templates/*.jade) ifndef DEST -DEST=bbctrl/ +DEST=mnt/ endif WATCH := src/jade src/jade/templates src/stylus src/js src/resources Makefile @@ -25,7 +26,7 @@ WATCH := src/jade src/jade/templates src/stylus src/js src/resources Makefile all: html css js static copy: all - cp -r *.py inevent http/ $(DEST) + cp -r *.py inevent $(TARGET)/ $(DEST) mount: mkdir -p $(DEST) @@ -37,10 +38,10 @@ umount: html: templates $(HTML) css: $(CSS_ASSETS) $(CSS_ASSETS).sha256 - install -D $< http/css/style-$(shell cat $(CSS_ASSETS).sha256).css + install -D $< $(TARGET)/css/style-$(shell cat $(CSS_ASSETS).sha256).css js: $(JS_ASSETS) $(JS_ASSETS).sha256 - install -D $< http/js/assets-$(shell cat $(JS_ASSETS).sha256).js + install -D $< $(TARGET)/js/assets-$(shell cat $(JS_ASSETS).sha256).js static: $(STATIC) @@ -54,7 +55,7 @@ build/hashes.jade: $(CSS_ASSETS).sha256 $(JS_ASSETS).sha256 echo "- var css_hash = '$(shell cat $(CSS_ASSETS).sha256)'" > $@ echo "- var js_hash = '$(shell cat $(JS_ASSETS).sha256)'" >> $@ -http/index.html: build/templates.jade build/hashes.jade +$(TARGET)/index.html: build/templates.jade build/hashes.jade $(JS_ASSETS): $(JS) node_modules @mkdir -p $(shell dirname $@) @@ -68,12 +69,12 @@ node_modules: mkdir -p $(shell dirname $@) sha256sum $< | sed 's/^\([a-f0-9]\+\) .*$$/\1/' > $@ -http/%: src/resources/% +$(TARGET)/%: src/resources/% install -D $< $@ -http/%.html: src/jade/%.jade $(wildcard src/jade/*.jade) node_modules +$(TARGET)/%.html: src/jade/%.jade $(wildcard src/jade/*.jade) node_modules @mkdir -p $(shell dirname $@) - $(JADE) -P $< -o http || (rm -f $@; exit 1) + $(JADE) -P $< -o $(TARGET) || (rm -f $@; exit 1) build/css/%.css: src/stylus/%.styl node_modules mkdir -p $(shell dirname $@) diff --git a/README.md b/README.md index d42b74b..323cc65 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,10 @@ ssh pi@ Substitute ```` with the correct IP address. The default password is ``raspberry``. You should see a prompt like this: ``pi@raspberrypi ~ $``, but in color. ## Configure the RPi -Copy the ``setup_rpi.sh`` script to the RPi and run it as root: +Copy the ``scripts/setup_rpi.sh`` script to the RPi and run it as root: ``` -scp setup_rpi.sh pi@: +scp scripts/setup_rpi.sh pi@: ssh pi@ sudo ./setup_rpi.sh ``` @@ -120,3 +120,6 @@ You should see a prompt. ``` avrdude -c avrispmkII -p ATxmega128A3U -P usb -U flash:w:tinyg.hex:i ``` + + +## Setup Python Development diff --git a/bbctrl.init.d b/bbctrl.init.d deleted file mode 100644 index b7a0fb6..0000000 --- a/bbctrl.init.d +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash - -### BEGIN INIT INFO -# Provides: bbctl -# Required-Start: $local_fs $network -# Required-Stop: $local_fs $network -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: Buildbotics Controller Web service -# Description: Buildbotics Controller Web service -### END INIT INFO - -DAEMON=/home/bbmc/bbctrl.py -DAEMON_NAME=bbctrl -DAEMON_OPTS="" -DAEMON_USER=root -DAEMON_DIR=$(dirname $DAEMON) -PIDFILE=/var/run/$DAEMON_NAME.pid - -. /lib/lsb/init-functions - - -do_start () { - log_daemon_msg "Starting system $DAEMON_NAME daemon" - 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" - log_end_msg $? -} - - -do_stop () { - log_daemon_msg "Stopping system $DAEMON_NAME daemon" - start-stop-daemon --stop --pidfile $PIDFILE --retry 10 - log_end_msg $? -} - - -case "$1" in - start|stop) do_${1} ;; - restart|reload|force-reload) do_stop; do_start ;; - status) - status_of_proc "$DAEMON_NAME" "$DAEMON" && exit 0 || exit $? - ;; - - *) - echo "Usage: /etc/init.d/$DAEMON_NAME {start|stop|restart|status}" - exit 1 - ;; -esac diff --git a/bbctrl.py b/bbctrl.py deleted file mode 100755 index 0a4e875..0000000 --- a/bbctrl.py +++ /dev/null @@ -1,362 +0,0 @@ -#!/usr/bin/env python3 - -## Change this to match your local settings -SERIAL_PORT = '/dev/ttyAMA0' -SERIAL_BAUDRATE = 115200 -HTTP_PORT = 8080 -HTTP_ADDR = '0.0.0.0' - -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 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 - } - - -with open('http/config-template.json', 'r', encoding = 'utf-8') as f: - config_template = json.load(f) - - -state = {} -clients = [] - -input_queue = multiprocessing.Queue() -output_queue = multiprocessing.Queue() - - -def on_exit(sig, func = None): - print('exit handler triggered') - 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('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, input_queue, output_queue): - multiprocessing.Process.__init__(self) - self.input_queue = input_queue - self.output_queue = output_queue - self.sp = serial.Serial(SERIAL_PORT, SERIAL_BAUDRATE, 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, '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) - -screen = lcd.LCD(1, 0x27) - - -def splash(): - 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.clear() - screen.display(1, 'Goodbye', lcd.JUSTIFY_CENTER) - - -if __name__ == "__main__": - signal.signal(signal.SIGTERM, on_exit) - - import logging - logging.getLogger().setLevel(logging.DEBUG) - - # Start the serial worker - try: - sp = SerialProcess(input_queue, output_queue) - sp.daemon = True - sp.start() - except Exception as e: - print(e) - - # Adjust the interval according to frames sent by serial port - ioloop.PeriodicCallback(checkQueue, 100).start() - ioloop.PeriodicCallback(checkEvents, 100).start() - - splash() - atexit.register(goodbye) - - # Start the web server - app = web.Application(router.urls + handlers) - app.listen(HTTP_PORT, address = HTTP_ADDR) - print('Listening on http://{}:{}/'.format(HTTP_ADDR, HTTP_PORT)) - ioloop.IOLoop.instance().start() diff --git a/inevent/AbsAxisScaling.py b/inevent/AbsAxisScaling.py deleted file mode 100644 index bd4baa1..0000000 --- a/inevent/AbsAxisScaling.py +++ /dev/null @@ -1,80 +0,0 @@ -# The inevent Python module was adapted from pi3d.event from the pi3d -# project. -# -# Copyright (c) 2016, Joseph Coffland, Cauldron Development LLC. -# Copyright (c) 2015, Tim Skillman. -# Copyright (c) 2015, Paddy Gaunt. -# Copyright (c) 2015, Tom Ritchford. -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -import array -import fcntl -import struct -from inevent import ioctl - - -def EVIOCGABS(axis): - return ioctl._IOR(ord('E'), 0x40 + axis, "ffffff") # get abs value/limits - - - -class AbsAxisScaling(object): - """ - Fetches and implements the EV_ABS axis scaling. - - The constructor fetches the scaling values from the given stream for the - given axis using an ioctl. - - There is a scale method, which scales a given value to the range -1..+1. - """ - - def __init__(self, stream, axis): - """ - Fetch the scale values for this stream and fill in the instance - variables accordingly. - """ - s = array.array("i", [1, 2, 3, 4, 5, 6]) - try: - fcntl.ioctl(stream.filehandle, EVIOCGABS(axis), s) - - except IOError: - self.value = self.minimum = self.maximum = self.fuzz = self.flat = \ - self.resolution = 1 - - else: - self.value, self.minimum, self.maximum, self.fuzz, self.flat, \ - self.resolution = struct.unpack("iiiiii", s) - - - def __str__(self): - return "Value {0} Min {1}, Max {2}, Fuzz {3}, Flat {4}, Res {5}".format( - self.value, self.minimum, self.maximum, self.fuzz, self.flat, - self.resolution) - - - def scale(self, value): - """ - scales the given value into the range -1..+1 - """ - return (float(value) - float(self.minimum)) / \ - float(self.maximum - self.minimum) * 2.0 - 1.0 - diff --git a/inevent/Constants.py b/inevent/Constants.py deleted file mode 100644 index 00be4cb..0000000 --- a/inevent/Constants.py +++ /dev/null @@ -1,97 +0,0 @@ -# The inevent Python module was adapted from pi3d.event from the pi3d -# project. -# -# Copyright (c) 2016, Joseph Coffland, Cauldron Development LLC. -# Copyright (c) 2015, Tim Skillman. -# Copyright (c) 2015, Paddy Gaunt. -# Copyright (c) 2015, Tom Ritchford. -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -EV_SYN = 0x00 -EV_KEY = 0x01 -EV_REL = 0x02 -EV_ABS = 0x03 -EV_MSC = 0x04 -EV_SW = 0x05 -EV_LED = 0x11 -EV_SND = 0x12 -EV_REP = 0x14 -EV_FF = 0x15 -EV_PWR = 0x16 -EV_FF_STATUS = 0x17 - -ev_type_name = {} -ev_type_name[EV_SYN] = "SYN" -ev_type_name[EV_KEY] = "KEY" -ev_type_name[EV_REL] = "REL" -ev_type_name[EV_ABS] = "ABS" -ev_type_name[EV_MSC] = "MSC" -ev_type_name[EV_SW] = "SW" -ev_type_name[EV_LED] = "LED" -ev_type_name[EV_SND] = "SND" -ev_type_name[EV_REP] = "REP" -ev_type_name[EV_FF] = "FF" -ev_type_name[EV_PWR] = "PWR" -ev_type_name[EV_FF_STATUS] = "FF_STATUS" - -SYN_REPORT = 0 -SYN_CONFIG = 1 - -REL_X = 0x00 -REL_Y = 0x01 -REL_Z = 0x02 -REL_RX = 0x03 -REL_RY = 0x04 -REL_RZ = 0x05 -REL_HWHEEL = 0x06 -REL_DIAL = 0x07 -REL_WHEEL = 0x08 -REL_MISC = 0x09 -REL_MAX = 0x0f - -ABS_X = 0x00 -ABS_Y = 0x01 -ABS_Z = 0x02 -ABS_RX = 0x03 -ABS_RY = 0x04 -ABS_RZ = 0x05 -ABS_THROTTLE = 0x06 -ABS_RUDDER = 0x07 -ABS_WHEEL = 0x08 -ABS_GAS = 0x09 -ABS_BRAKE = 0x0a -ABS_HAT0X = 0x10 -ABS_HAT0Y = 0x11 -ABS_HAT1X = 0x12 -ABS_HAT1Y = 0x13 -ABS_HAT2X = 0x14 -ABS_HAT2Y = 0x15 -ABS_HAT3X = 0x16 -ABS_HAT3Y = 0x17 -ABS_PRESSURE = 0x18 -ABS_DISTANCE = 0x19 -ABS_TILT_X = 0x1a -ABS_TILT_Y = 0x1b -ABS_TOOL_WIDTH = 0x1c -ABS_VOLUME = 0x20 -ABS_MISC = 0x28 -ABS_MAX = 0x3f diff --git a/inevent/Event.py b/inevent/Event.py deleted file mode 100644 index c14e9d4..0000000 --- a/inevent/Event.py +++ /dev/null @@ -1,115 +0,0 @@ -# The inevent Python module was adapted from pi3d.event from the pi3d -# project. -# -# Copyright (c) 2016, Joseph Coffland, Cauldron Development LLC. -# Copyright (c) 2015, Tim Skillman. -# Copyright (c) 2015, Paddy Gaunt. -# Copyright (c) 2015, Tom Ritchford. -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -import struct - -from inevent import Format -from inevent.Constants import * - - - -class Event(object): - """ - A single event from the linux input event system. - - Events are tuples: (Time, Type, Code, Value) - In addition we remember the stream it came from. - - Externally, only the unhandled event handler gets passed the whole event, - but the SYN handler gets the code and value. (Also the keyboard handler, but - those are renamed to key and value.) - - This class is responsible for converting the Linux input event structure into - one of these objects and back again. - """ - def __init__(self, stream, time = None, type = None, code = None, - value = None): - """ - Create a new event. - - Generally all but the stream parameter are left out; we will want to - populate the object from a Linux input event using decode. - """ - self.stream = stream - self.time = time - self.type = type - self.code = code - self.value = value - - - def get_type_name(self): - if self.type not in ev_type_name: return '0x%x' % self.type - return ev_type_name[self.type] - - - def get_source(self): - return "%s[%d]" % (self.stream.devType, self.stream.devIndex) - - - def __str__(self): - """ - Uses the stream to give the device type and whether it is currently grabbed. - """ - grabbed = "grabbed" if self.stream.grabbed else "ungrabbed" - - return "Event %s %s @%f: %s 0x%x=0x%x" % ( - self.get_source(), grabbed, self.time, self.get_type_name(), self.code, - self.value) - - - def __repr__(self): - return "Event(%s, %f, 0x%x, 0x%x, 0x%x)" % ( - repr(self.stream), self.time, self.type, self.code, self.value) - - - def encode(self): - """ - Encode this event into a Linux input event structure. - - The output is packed into a string. It is unlikely that this function - will be required, but it might as well be here. - """ - tint = long(self.time) - tfrac = long((self.time - tint) * 1000000) - - return \ - struct.pack(Format.Event, tsec, tfrac, self.type, self.code, self.value) - - - def decode(self, s): - """ - Decode a Linux input event into the fields of this object. - - Arguments: - *s* - A binary structure packed into a string. - """ - tsec, tfrac, self.type, self.code, self.value = \ - struct.unpack(Format.Event, s) - - self.time = tsec + tfrac / 1000000.0 diff --git a/inevent/EventHandler.py b/inevent/EventHandler.py deleted file mode 100644 index 97642503..0000000 --- a/inevent/EventHandler.py +++ /dev/null @@ -1,119 +0,0 @@ -# The inevent Python module was adapted from pi3d.event from the pi3d -# project. -# -# Copyright (c) 2016, Joseph Coffland, Cauldron Development LLC. -# Copyright (c) 2015, Tim Skillman. -# Copyright (c) 2015, Paddy Gaunt. -# Copyright (c) 2015, Tom Ritchford. -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from inevent.Constants import * -from inevent.EventStream import EventStream - - -class EventHandler(object): - """ - A class to handle events. - - Four types of events are handled: REL (mouse movement), KEY (keybaord keys and - other device buttons), ABS (joysticks and gamepad analogue sticks) and SYN - (delimits simultaneous events such as mouse movements) - """ - def __init__(self): - self.buttons = dict() - - - def event(self, event, handler): - """ - Handles the given event. - - If the event is passed to a handler or otherwise handled then returns None, - else returns the event. All handlers are optional. - - All key events are handled by putting them in the self.buttons dict, and - optionally by calling the supplied handler. - - REL X, Y and wheel V and H events are all accumulated internally and - also optionally passed to the supplied handler. All these events are - handled. - - ABS X, Y, Z, RX, RY, RZ, Hat0X, Hat0Y are all accumulated internally and - also optionally passed to the supplied handler. Other ABS events are not - handled. - - All SYN events are passed to the supplied handler. - - There are several ABS events that we do not handle. In particular: - THROTTLE, RUDDER, WHEEL, GAS, BRAKE, HAT1, HAT2, HAT3, PRESSURE, - DISTANCE, TILT, TOOL_WIDTH. Implementing these is left as an exercise - for the interested reader. - - Likewise, since one handler is handling all events for all devices, we - may get the situation where two devices return the same button. The only - way to handle that would seem to be to have a key dict for every device, - which seems needlessly profligate for a situation that may never arise. - """ - - state = event.stream.state - - if event.type == EV_KEY: self.buttons[event.code] = event.value - elif event.type == EV_REL: state.rel[event.code] += event.value - elif event.type == EV_ABS: - state.abs[event.code] = event.stream.scale(event.code, event.value) - - if handler: handler(event, state) - - - def key_state(self, code): - """ - Returns the last event value for the given key code. - - Key names can be converted to key codes using codeOf[str]. - If the key is pressed the returned value will be 1 (pressed) or 2 (held). - If the key is not pressed, the returned value will be 0. - """ - return self.buttons.get(code, 0) - - - def clear_key(self, code): - """ - Clears the event value for the given key code. - - Key names can be converted to key codes using codeOf[str]. - This emulates a key-up but does not generate any events. - """ - self.buttons[code] = 0 - - - def get_keys(self): - """ - Returns the first of whichever keys have been pressed. - - Key names can be converted to key codes using codeOf[str]. - This emulates a key-up but does not generate any events. - """ - k_list = [] - - for k in self.buttons: - if self.buttons[k] != 0: k_list.append(k) - - return k_list diff --git a/inevent/EventState.py b/inevent/EventState.py deleted file mode 100644 index a87ae7b..0000000 --- a/inevent/EventState.py +++ /dev/null @@ -1,118 +0,0 @@ -# The inevent Python module was adapted from pi3d.event from the pi3d -# project. -# -# Copyright (c) 2016, Joseph Coffland, Cauldron Development LLC. -# Copyright (c) 2015, Tim Skillman. -# Copyright (c) 2015, Paddy Gaunt. -# Copyright (c) 2015, Tom Ritchford. -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from inevent.Constants import * - - -class EventState: - def __init__(self): - self.abs = [0.0] * ABS_MAX - self.rel = [0.0] * REL_MAX - - - def __str__(self): - return ("({:6.3f}, {:6.3f}, {:6.3f}) ".format(*self.get_joystick3d()) + - "({:6.3f}, {:6.3f}, {:6.3f}) ".format(*self.get_joystickR3d()) + - "({:2.0f}, {:2.0f}) ".format(*self.get_hat()) + - "({:d}, {:d}) ".format(*self.get_mouse()) + - "({:d}, {:d})".format(*self.get_wheel())) - - - def get_joystick(self): - """ - Returns the x,y coordinates for a joystick or left gamepad analogue stick. - - The values are returned as a tuple. All values are -1.0 to +1.0 with - 0.0 being centred. - """ - return self.abs[ABS_X], self.abs[ABS_Y] - - - def get_joystick3d(self): - """ - Returns the x,y,z coordinates for a joystick or left gamepad analogue stick - - The values are returned as a tuple. All values are -1.0 to +1.0 with - 0.0 being centred. - """ - return self.abs[ABS_X], self.abs[ABS_Y], self.abs[ABS_Z] - - - def get_joystickR(self): - """ - Returns the x,y coordinates for a right gamepad analogue stick. - - The values are returned as a tuple. For some odd reason, the gamepad - returns values in the Z axes of both joysticks, with y being the first. - - All values are -1.0 to +1.0 with 0.0 being centred. - """ - return self.abs[ABS_RZ], self.abs[ABS_Z] - - - def get_joystickR3d(self): - """ - Returns the x,y,z coordinates for a 2nd joystick control - - The values are returned as a tuple. All values are -1.0 to +1.0 with - 0.0 being centred. - """ - return self.abs[ABS_RX], self.abs[ABS_RY], self.abs[ABS_RZ] - - - def get_hat(self): - """ - Returns the x,y coordinates for a joystick hat or gamepad direction pad - - The values are returned as a tuple. All values are -1.0 to +1.0 with - 0.0 being centred. - """ - return self.abs[ABS_HAT0X], self.abs[ABS_HAT0Y] - - - def get_mouse(self): - return self.rel[REL_X], self.rel[REL_Y] - - - def get_wheel(self): - return self.rel[REL_WHEEL], self.rel[REL_HWHEEL] - - - def get_mouse_movement(self): - """ - Returns the accumulated REL (mouse or other relative device) movements - since the last call. - - The returned value is a tuple: (X, Y, WHEEL, H-WHEEL) - """ - ret = self.get_mouse() + self.get_wheel() - - self.rel[REL_X] = self.rel[REL_Y] = 0 - self.rel[REL_WHEEL] = self.rel[REL_HWHEEL] = 0 - - return ret diff --git a/inevent/EventStream.py b/inevent/EventStream.py deleted file mode 100644 index 408e5ec..0000000 --- a/inevent/EventStream.py +++ /dev/null @@ -1,184 +0,0 @@ -# The inevent Python module was adapted from pi3d.event from the pi3d -# project. -# -# Copyright (c) 2016, Joseph Coffland, Cauldron Development LLC. -# Copyright (c) 2015, Tim Skillman. -# Copyright (c) 2015, Paddy Gaunt. -# Copyright (c) 2015, Tom Ritchford. -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -import fcntl -import os -import select - -from inevent.Constants import * -from inevent import ioctl -from inevent.AbsAxisScaling import AbsAxisScaling -from inevent import Event -from inevent import Format -from inevent.EventState import EventState - - -EVIOCGRAB = ioctl._IOW(ord('E'), 0x90, "i") # Grab/Release device - - - -class EventStream(object): - """ - encapsulates the event* file handling - - Each device is represented by a file in /dev/input called eventN, where N is - a small number. (Actually, a keybaord can be represented by two such files.) - Instances of this class open one of these files and provide means to read - events from them. - - Class methods also exist to read from multiple files simultaneously, and - also to grab and ungrab all instances of a given type. - """ - axisX = 0 - axisY = 1 - axisZ = 2 - axisRX = 3 - axisRY = 4 - axisRZ = 5 - axisHat0X = 6 - axisHat0Y = 7 - axisHat1X = 8 - axisHat1Y = 9 - axisHat2X = 10 - axisHat2Y = 11 - axisHat3X = 12 - axisHat3Y = 13 - axisThrottle = 14 - axisRudder = 15 - axisWheel = 16 - axisGas = 17 - axisBrake = 18 - axisPressure = 19 - axisDistance = 20 - axisTiltX = 21 - axisTiltY = 22 - axisToolWidth = 23 - numAxes = 24 - - axisToEvent = [ - ABS_X, ABS_Y, ABS_Z, ABS_RX, ABS_RY, ABS_RZ, ABS_HAT0X, ABS_HAT0Y, - ABS_HAT1X, ABS_HAT1Y, ABS_HAT2X, ABS_HAT2Y, ABS_HAT3X, ABS_HAT3Y, - ABS_THROTTLE, ABS_RUDDER, ABS_WHEEL, ABS_GAS, ABS_BRAKE, ABS_PRESSURE, - ABS_DISTANCE, ABS_TILT_X, ABS_TILT_Y, ABS_TOOL_WIDTH] - - - def __init__(self, devIndex, devType): - """ - Opens the given /dev/input/event file and grabs it. - - Also adds it to a class-global list of all existing streams. - """ - self.devIndex = devIndex - self.devType = devType - self.filename = "/dev/input/event" + str(devIndex) - self.filehandle = os.open(self.filename, os.O_RDWR) - self.state = EventState() - self.grab(True) - self.absInfo = [None] * ABS_MAX - - if devType == "js": - for axis in range(ABS_MAX): - self.absInfo[axis] = AbsAxisScaling(self, axis) - - - def scale(self, axis, value): - """ - Scale the given value according to the given axis. - - acquire_abs_info must have been previously called to acquire the data to - do the scaling. - """ - assert axis < ABS_MAX, "Axis number out of range" - - if self.absInfo[axis]: return self.absInfo[axis].scale(value) - else: return value - - - def grab(self, grab = True): - """ - Grab (or release) exclusive access to all devices of the given type. - - The devices are grabbed if grab is True and released if grab is False. - - All devices are grabbed to begin with. We might want to ungrab the - keyboard for example to use it for text entry. While not grabbed, all - key-down and key-hold events are filtered out. - """ - fcntl.ioctl(self.filehandle, EVIOCGRAB, 1 if grab else 0) - self.grabbed = grab - - - def __iter__(self): - """s - Required to make this class an iterator - """ - return self - - - def next(self): - """ - Returns the next waiting event. - - If no event is waiting, returns None. - """ - ready = select.select([self.filehandle], [], [], 0)[0] - if ready: return self.read() - - - def read(self): - """ - Read and return the next waiting event. - """ - try: - s = os.read(self.filehandle, Format.EventSize) - if s: - event = Event.Event(self) - event.decode(s) - return event - - except: pass - - - def __enter__(self): return self - - - def release(self): - "Ungrabs the file and closes it." - - try: - self.grab(False) - os.close(self.filehandle) - - except: - pass - - - def __exit__(self, type, value, traceback): - "Ungrabs the file and closes it." - - self.release() diff --git a/inevent/FindDevices.py b/inevent/FindDevices.py deleted file mode 100644 index 17a667a..0000000 --- a/inevent/FindDevices.py +++ /dev/null @@ -1,215 +0,0 @@ -# The inevent Python module was adapted from pi3d.event from the pi3d -# project. -# -# Copyright (c) 2016, Joseph Coffland, Cauldron Development LLC. -# Copyright (c) 2015, Tim Skillman. -# Copyright (c) 2015, Paddy Gaunt. -# Copyright (c) 2015, Tom Ritchford. -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -import re -from inevent.Constants import * - - -def test_bit(nlst, b): - index = b / 32 - bit = b % 32 - return index < len(nlst) and nlst[index] & (1 << bit) - - -def EvToStr(events): - s = [] - - if test_bit(events, EV_SYN): s.append("EV_SYN") - if test_bit(events, EV_KEY): s.append("EV_KEY") - if test_bit(events, EV_REL): s.append("EV_REL") - if test_bit(events, EV_ABS): s.append("EV_ABS") - if test_bit(events, EV_MSC): s.append("EV_MSC") - if test_bit(events, EV_LED): s.append("EV_LED") - if test_bit(events, EV_SND): s.append("EV_SND") - if test_bit(events, EV_REP): s.append("EV_REP") - 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 - - -class DeviceCapabilities(object): - def __init__(self, firstLine, filehandle): - self.EV_SYNevents = [] - self.EV_KEYevents = [] - self.EV_RELevents = [] - self.EV_ABSevents = [] - self.EV_MSCevents = [] - self.EV_LEDevents = [] - self.EV_SNDevents = [] - self.EV_REPevents = [] - self.EV_FFevents = [] - 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) - self.bus = 0 - self.vendor = 0 - self.product = 0 - self.version = 0 - - else: - self.bus = int(match.group(1), base = 16) - self.vendor = int(match.group(2), base = 16) - self.product = int(match.group(3), base = 16) - self.version = int(match.group(4), base = 16) - - for line in filehandle: - if not line.strip(): break - - if line[0] == "N": - match = re.search('Name="([^"]+)"', line) - if match: self.name = match.group(1) - else: self.name = "UNKNOWN" - - elif line[0] == "P": - match = re.search('Phys=(.+)', line) - if match: self.phys = match.group(1) - else: self.phys = "UNKNOWN" - - elif line[0] == "S": - match = re.search('Sysfs=(.+)', line) - if match: self.sysfs = match.group(1) - else: self.sysfs = "UNKNOWN" - - elif line[0] == "U": - match = re.search('Uniq=(.*)', line) - if match: self.uniq = match.group(1) - else: self.uniq = "UNKNOWN" - - elif line[0] == "H": - match = re.search('Handlers=(.+)', line) - if match: self.handlers = match.group(1).split() - else: self.handlers = [] - - elif line[:5] == "B: EV": - eventsNums = [int(x, base = 16) for x in line[6:].split()] - eventsNums.reverse() - self.eventTypes = eventsNums - - elif line[:6] == "B: KEY": - eventsNums = [int(x, base = 16) for x in line[7:].split()] - eventsNums.reverse() - self.EV_KEYevents = eventsNums - - elif line[:6] == "B: ABS": - eventsNums = [int(x, base = 16) for x in line[7:].split()] - eventsNums.reverse() - self.EV_ABSevents = eventsNums - - elif line[:6] == "B: MSC": - eventsNums = [int(x, base = 16) for x in line[7:].split()] - eventsNums.reverse() - self.EV_MSCevents = eventsNums - - elif line[:6] == "B: REL": - eventsNums = [int(x, base = 16) for x in line[7:].split()] - eventsNums.reverse() - self.EV_RELevents = eventsNums - - elif line[:6] == "B: LED": - eventsNums = [int(x, base = 16) for x in line[7:].split()] - eventsNums.reverse() - self.EV_LEDevents = eventsNums - - for handler in self.handlers: - if handler[:5] == "event": self.eventIndex = int(handler[5:]) - - self.isMouse = False - self.isKeyboard = False - self.isJoystick = False - - - def doesProduce(self, eventType, eventCode): - return test_bit(self.eventTypes, eventType) and ( - (eventType == EV_SYN and test_bit(self.EV_SYNevents, eventCode)) or - (eventType == EV_KEY and test_bit(self.EV_KEYevents, eventCode)) or - (eventType == EV_REL and test_bit(self.EV_RELevents, eventCode)) or - (eventType == EV_ABS and test_bit(self.EV_ABSevents, eventCode)) or - (eventType == EV_MSC and test_bit(self.EV_MSCevents, eventCode)) or - (eventType == EV_LED and test_bit(self.EV_LEDevents, eventCode)) or - (eventType == EV_SND and test_bit(self.EV_SNDevents, eventCode)) or - (eventType == EV_REP and test_bit(self.EV_REPevents, eventCode)) or - (eventType == EV_FF and test_bit(self.EV_FFevents, eventCode)) or - (eventType == EV_PWR and test_bit(self.EV_PWRevents, eventCode)) or - (eventType == EV_FF_STATUS and - test_bit(self.EV_FF_STATUSevents, eventCode))) - - - def __str__(self): - return ( - "%s\n" - "Bus: %s Vendor: %s Product: %s Version: %s\n" - "Phys: %s\n" - "Sysfs: %s\n" - "Uniq: %s\n" - "Handlers: %s Event Index: %s\n" - "Keyboard: %s Mouse: %s Joystick: %s\n" - "Events: %s" % ( - self.name, self.bus. self.vendor, self.product, self.version, self.phys, - self.sysfs, self.uniq, self.handlers, self.eventIndex, self.isKeyboard, - self.isMouse, self.isJoystick, EvToStr(self.eventTypes))) - - -deviceCapabilities = [] - - -def get_devices(filename = "/proc/bus/input/devices"): - global deviceCapabilities - - with open("/proc/bus/input/devices", "r") as filehandle: - for line in filehandle: - if line[0] == "I": - deviceCapabilities.append(DeviceCapabilities(line, filehandle)) - - return deviceCapabilities - - -def print_devices(): - devs = get_devices() - - for dev in devs: - print(str(dev)) - print(" ABS: {}" - .format([x for x in range(64) if test_bit(dev.EV_ABSevents, x)])) - print(" REL: {}" - .format([x for x in range(64) if test_bit(dev.EV_RELevents, x)])) - print(" MSC: {}" - .format([x for x in range(64) if test_bit(dev.EV_MSCevents, x)])) - print(" KEY: {}" - .format([x for x in range(512) if test_bit(dev.EV_KEYevents, x)])) - print(" LED: {}" - .format([x for x in range(64) if test_bit(dev.EV_LEDevents, x)])) - print() diff --git a/inevent/Format.py b/inevent/Format.py deleted file mode 100644 index d91665e..0000000 --- a/inevent/Format.py +++ /dev/null @@ -1,32 +0,0 @@ -# The inevent Python module was adapted from pi3d.event from the pi3d -# project. -# -# Copyright (c) 2016, Joseph Coffland, Cauldron Development LLC. -# Copyright (c) 2015, Tim Skillman. -# Copyright (c) 2015, Paddy Gaunt. -# Copyright (c) 2015, Tom Ritchford. -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -import struct - -Event = 'llHHi' -EventSize = struct.calcsize(Event) diff --git a/inevent/InEvent.py b/inevent/InEvent.py deleted file mode 100644 index fab068a..0000000 --- a/inevent/InEvent.py +++ /dev/null @@ -1,252 +0,0 @@ -# The inevent Python module was adapted from pi3d.event from the pi3d -# project. -# -# Copyright (c) 2016, Joseph Coffland, Cauldron Development LLC. -# Copyright (c) 2015, Tim Skillman. -# Copyright (c) 2015, Paddy Gaunt. -# Copyright (c) 2015, Tom Ritchford. -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -import pyudev -import re -import select -import errno - -from inevent.EventHandler import EventHandler -from inevent import Keys -from inevent.Constants import * -from inevent.EventStream import EventStream - - -_KEYS = (k for k in vars(Keys) if not k.startswith('_')) -KEY_CODE = dict((k, getattr(Keys, k)) for k in _KEYS) -CODE_KEY = {} -for v in KEY_CODE: CODE_KEY[KEY_CODE[v]] = v - - -def key_to_code(key): - return KEY_CODE.get(str(key), -1) \ - if isinstance(key, str) else key - - -def code_to_key(code): return CODE_KEY.get(code, '') - - -def find_devices(types): - """Finds the event indices of all devices of the specified types. - - A type is a string on the handlers line of /proc/bus/input/devices. - Keyboards use "kbd", mice use "mouse" and joysticks (and gamepads) use "js". - - Returns a list of integer indexes N, where /dev/input/eventN is the event - stream for each device. - - If butNot is given it holds a list of tuples which the returned values should - not match. - - All devices of each type are returned; if you have two mice, they will both - be used. - """ - with open("/proc/bus/input/devices", "r") as filehandle: - for line in filehandle: - if line[0] == "H": - for type in types: - if type in line: - match = re.search("event([0-9]+)", line) - index = match and match.group(1) - if index: yield int(index), type - break - - - -class InEvent(object): - """Encapsulates the entire InEvent subsystem. - - This is generally all you need to import. - - On instantiation, we open all devices that are keyboards, mice or joysticks. - That means we might have two of one sort of another, and that might be a - problem, but it would be rather rare. - - There are several ABS (joystick, touch) events that we do not handle, - specifically THROTTLE, RUDDER, WHEEL, GAS, BRAKE, HAT1, HAT2, HAT3, PRESSURE, - DISTANCE, TILT, TOOL_WIDTH. Implementing these is left as an exercise - for the interested reader. Similarly, we make no attempt to handle - multi-touch. - - Handlers can be supplied, in which case they are called for each event, but - it isn't necessary; API exists for all the events. - - The handler signature is: - - def handler_func(event, state) - - where: - event: - An Event object describing the event. - - state: - An EventState object describing the current state. - - Use key_to_code() to convert from the name of a key to its code, - and code_to_key() to convert a code to a name. - - 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"]): - self.streams = [] - self.handler = EventHandler() - self.types = types - - devs = list(find_devices(types)) - 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() - - - def process_udev_event(self): - action, device = self.udevMon.receive_device() - - match = re.search(r"/dev/input/event([0-9]+)", str(device.device_node)) - devIndex = match and match.group(1) - if not devIndex: return - devIndex = int(devIndex) - - if action == 'add': - for index, devType in find_devices(self.types): - if index == devIndex: - self.add_stream(devIndex, devType) - break - - if action == 'remove': - 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] - - # 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 add_stream(self, devIndex, devType): - try: - self.streams.append(EventStream(devIndex, devType)) - print('Added {}[{:d}]'.format(devType, devIndex)) - - except OSError as e: - if not e.errno in [errno.EPERM, errno.EACCES]: 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 - - - def key_state(self, key): - """ - Returns the state of the given key. - - The returned value will be 0 for key-up, or 1 for key-down. This method - returns a key-held(2) as 1 to aid in using the returned value as a - movement distance. - - This function accepts either the key code or the string name of the key. - It would be more efficient to look-up and store the code of - the key with KEY_CODE[], rather than using the string every time. (Which - involves a dict look-up keyed with a string for every key_state call, every - time around the loop.) - - Gamepad keys are: - Select = BTN_BASE3, Start = BTN_BASE4 - L1 = BTN_TOP R1 = BTN_BASE - L2 = BTN_PINKIE R2 = BTN_BASE2 - - The action buttons are: - BTN_THUMB - BTN_TRIGGER - BTN_TOP - BTN_THUMB2 - - Analogue Left Button = BTN_BASE5 - Analogue Right Button = BTN_BASE6 - - Some of those may clash with extended mouse buttons, so if you are using - both at once, you'll see some overlap. - - The direction pad is hat0 (see get_hat) - """ - return self.handler.key_state(key_to_code(key)) - - - def clear_key(self, key): - """ - Clears the state of the given key. - - Emulates a key-up, but does not call any handlers. - """ - return self.handler.clear_key(key_to_code(key)) - - - def get_keys(self): - return [code_to_key(k) for k in self.handler.get_keys()] - - - def release(self): - """ - Ungrabs all streams and closes all files. - - 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/inevent/JogHandler.py b/inevent/JogHandler.py deleted file mode 100644 index f14b55d..0000000 --- a/inevent/JogHandler.py +++ /dev/null @@ -1,82 +0,0 @@ -from inevent.Constants import * - - -def axes_to_string(axes): - return "({:6.3f}, {:6.3f}, {:6.3f}, {:6.3f})".format(*axes) - - -def print_event(event, state): - print("{} {}: ".format(event.get_source(), event.get_type_name()), end = '') - - 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())) - - if event.type == EV_REL: - print("({: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)) - - -class JogHandler: - def __init__(self, config): - self.config = config - self.axes = [0.0, 0.0, 0.0, 0.0] - self.speed = 3 - self.activate = 0 - - - def changed(self): - print(axes_to_string(self.axes) + " x {:d}".format(self.speed)) - - - def __call__(self, event, state): - if event.type not in [EV_ABS, EV_REL, EV_KEY]: return - - changed = False - - # Process event - if event.type == EV_ABS and event.code in self.config['axes']: - pass - - elif event.type == EV_ABS and event.code in self.config['arrows']: - axis = self.config['arrows'].index(event.code) - - if event.value < 0: - if axis == 1: print('up') - else: print('left') - - elif 0 < event.value: - if axis == 1: print('down') - else: print('right') - - elif event.type == EV_KEY and event.code in self.config['speed']: - old_speed = self.speed - self.speed = self.config['speed'].index(event.code) + 1 - if self.speed != old_speed: changed = True - - elif event.type == EV_KEY and event.code in self.config['activate']: - index = self.config['activate'].index(event.code) - - if event.value: self.activate |= 1 << index - else: self.activate &= ~(1 << index) - - if self.config.get('verbose', False): print_event(event, state) - - # Update axes - old_axes = list(self.axes) - - for axis in range(4): - self.axes[axis] = event.stream.state.abs[self.config['axes'][axis]] - if abs(self.axes[axis]) < self.config['deadband']: - self.axes[axis] = 0 - if not (1 << axis) & self.activate and self.activate: - self.axes[axis] = 0 - - if old_axes != self.axes: changed = True - - if changed: self.changed() diff --git a/inevent/Keys.py b/inevent/Keys.py deleted file mode 100644 index 52b8543..0000000 --- a/inevent/Keys.py +++ /dev/null @@ -1,418 +0,0 @@ -# The inevent Python module was adapted from pi3d.event from the pi3d -# project. -# -# Copyright (c) 2016, Joseph Coffland, Cauldron Development LLC. -# Copyright (c) 2015, Tim Skillman. -# Copyright (c) 2015, Paddy Gaunt. -# Copyright (c) 2015, Tom Ritchford. -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -KEY_ESC = 1 -KEY_1 = 2 -KEY_2 = 3 -KEY_3 = 4 -KEY_4 = 5 -KEY_5 = 6 -KEY_6 = 7 -KEY_7 = 8 -KEY_8 = 9 -KEY_9 = 10 -KEY_0 = 11 -KEY_MINUS = 12 -KEY_EQUAL = 13 -KEY_BACKSPACE = 14 -KEY_TAB = 15 -KEY_Q = 16 -KEY_W = 17 -KEY_E = 18 -KEY_R = 19 -KEY_T = 20 -KEY_Y = 21 -KEY_U = 22 -KEY_I = 23 -KEY_O = 24 -KEY_P = 25 -KEY_LEFTBRACE = 26 -KEY_RIGHTBRACE = 27 -KEY_ENTER = 28 -KEY_LEFTCTRL = 29 -KEY_A = 30 -KEY_S = 31 -KEY_D = 32 -KEY_F = 33 -KEY_G = 34 -KEY_H = 35 -KEY_J = 36 -KEY_K = 37 -KEY_L = 38 -KEY_SEMICOLON = 39 -KEY_APOSTROPHE = 40 -KEY_GRAVE = 41 -KEY_LEFTSHIFT = 42 -KEY_BACKSLASH = 43 -KEY_Z = 44 -KEY_X = 45 -KEY_C = 46 -KEY_V = 47 -KEY_B = 48 -KEY_N = 49 -KEY_M = 50 -KEY_COMMA = 51 -KEY_DOT = 52 -KEY_SLASH = 53 -KEY_RIGHTSHIFT = 54 -KEY_KPASTERISK = 55 -KEY_LEFTALT = 56 -KEY_SPACE = 57 -KEY_CAPSLOCK = 58 -KEY_F1 = 59 -KEY_F2 = 60 -KEY_F3 = 61 -KEY_F4 = 62 -KEY_F5 = 63 -KEY_F6 = 64 -KEY_F7 = 65 -KEY_F8 = 66 -KEY_F9 = 67 -KEY_F10 = 68 -KEY_NUMLOCK = 69 -KEY_SCROLLLOCK = 70 -KEY_KP7 = 71 -KEY_KP8 = 72 -KEY_KP9 = 73 -KEY_KPMINUS = 74 -KEY_KP4 = 75 -KEY_KP5 = 76 -KEY_KP6 = 77 -KEY_KPPLUS = 78 -KEY_KP1 = 79 -KEY_KP2 = 80 -KEY_KP3 = 81 -KEY_KP0 = 82 -KEY_KPDOT = 83 - -KEY_ZENKAKUHANKAKU = 85 -KEY_102ND = 86 -KEY_F11 = 87 -KEY_F12 = 88 -KEY_RO = 89 -KEY_KATAKANA = 90 -KEY_HIRAGANA = 91 -KEY_HENKAN = 92 -KEY_KATAKANAHIRAGANA = 93 -KEY_MUHENKAN = 94 -KEY_KPJPCOMMA = 95 -KEY_KPENTER = 96 -KEY_RIGHTCTRL = 97 -KEY_KPSLASH = 98 -KEY_SYSRQ = 99 -KEY_RIGHTALT = 100 -KEY_LINEFEED = 101 -KEY_HOME = 102 -KEY_UP = 103 -KEY_PAGEUP = 104 -KEY_LEFT = 105 -KEY_RIGHT = 106 -KEY_END = 107 -KEY_DOWN = 108 -KEY_PAGEDOWN = 109 -KEY_INSERT = 110 -KEY_DELETE = 111 -KEY_MACRO = 112 -KEY_MUTE = 113 -KEY_VOLUMEDOWN = 114 -KEY_VOLUMEUP = 115 -KEY_POWER = 116 -KEY_KPEQUAL = 117 -KEY_KPPLUSMINUS = 118 -KEY_PAUSE = 119 - -KEY_KPCOMMA = 121 -KEY_HANGUEL = 122 -KEY_HANJA = 123 -KEY_YEN = 124 -KEY_LEFTMETA = 125 -KEY_RIGHTMETA = 126 -KEY_COMPOSE = 127 - -KEY_STOP = 128 -KEY_AGAIN = 129 -KEY_PROPS = 130 -KEY_UNDO = 131 -KEY_FRONT = 132 -KEY_COPY = 133 -KEY_OPEN = 134 -KEY_PASTE = 135 -KEY_FIND = 136 -KEY_CUT = 137 -KEY_HELP = 138 -KEY_MENU = 139 -KEY_CALC = 140 -KEY_SETUP = 141 -KEY_SLEEP = 142 -KEY_WAKEUP = 143 -KEY_FILE = 144 -KEY_SENDFILE = 145 -KEY_DELETEFILE = 146 -KEY_XFER = 147 -KEY_PROG1 = 148 -KEY_PROG2 = 149 -KEY_WWW = 150 -KEY_MSDOS = 151 -KEY_COFFEE = 152 -KEY_DIRECTION = 153 -KEY_CYCLEWINDOWS = 154 -KEY_MAIL = 155 -KEY_BOOKMARKS = 156 -KEY_COMPUTER = 157 -KEY_BACK = 158 -KEY_FORWARD = 159 -KEY_CLOSECD = 160 -KEY_EJECTCD = 161 -KEY_EJECTCLOSECD = 162 -KEY_NEXTSONG = 163 -KEY_PLAYPAUSE = 164 -KEY_PREVIOUSSONG = 165 -KEY_STOPCD = 166 -KEY_RECORD = 167 -KEY_REWIND = 168 -KEY_PHONE = 169 -KEY_ISO = 170 -KEY_CONFIG = 171 -KEY_HOMEPAGE = 172 -KEY_REFRESH = 173 -KEY_EXIT = 174 -KEY_MOVE = 175 -KEY_EDIT = 176 -KEY_SCROLLUP = 177 -KEY_SCROLLDOWN = 178 -KEY_KPLEFTPAREN = 179 -KEY_KPRIGHTPAREN = 180 - -KEY_F13 = 183 -KEY_F14 = 184 -KEY_F15 = 185 -KEY_F16 = 186 -KEY_F17 = 187 -KEY_F18 = 188 -KEY_F19 = 189 -KEY_F20 = 190 -KEY_F21 = 191 -KEY_F22 = 192 -KEY_F23 = 193 -KEY_F24 = 194 - -KEY_PLAYCD = 200 -KEY_PAUSECD = 201 -KEY_PROG3 = 202 -KEY_PROG4 = 203 -KEY_SUSPEND = 205 -KEY_CLOSE = 206 -KEY_PLAY = 207 -KEY_FASTFORWARD = 208 -KEY_BASSBOOST = 209 -KEY_PRINT = 210 -KEY_HP = 211 -KEY_CAMERA = 212 -KEY_SOUND = 213 -KEY_QUESTION = 214 -KEY_EMAIL = 215 -KEY_CHAT = 216 -KEY_SEARCH = 217 -KEY_CONNECT = 218 -KEY_FINANCE = 219 -KEY_SPORT = 220 -KEY_SHOP = 221 -KEY_ALTERASE = 222 -KEY_CANCEL = 223 -KEY_BRIGHTNESSDOWN = 224 -KEY_BRIGHTNESSUP = 225 -KEY_MEDIA = 226 - -KEY_UNKNOWN = 240 - -BTN_MISC = 0x100 -BTN_0 = 0x100 -BTN_1 = 0x101 -BTN_2 = 0x102 -BTN_3 = 0x103 -BTN_4 = 0x104 -BTN_5 = 0x105 -BTN_6 = 0x106 -BTN_7 = 0x107 -BTN_8 = 0x108 -BTN_9 = 0x109 - -BTN_MOUSE = 0x110 -BTN_LEFT = 0x110 -BTN_RIGHT = 0x111 -BTN_MIDDLE = 0x112 -BTN_SIDE = 0x113 -BTN_EXTRA = 0x114 -BTN_FORWARD = 0x115 -BTN_BACK = 0x116 -BTN_TASK = 0x117 - -BTN_JOYSTICK = 0x120 -BTN_TRIGGER = 0x120 -BTN_THUMB = 0x121 -BTN_THUMB2 = 0x122 -BTN_TOP = 0x123 -BTN_TOP2 = 0x124 -BTN_PINKIE = 0x125 -BTN_BASE = 0x126 -BTN_BASE2 = 0x127 -BTN_BASE3 = 0x128 -BTN_BASE4 = 0x129 -BTN_BASE5 = 0x12a -BTN_BASE6 = 0x12b -BTN_DEAD = 0x12f - -BTN_GAMEPAD = 0x130 -BTN_A = 0x130 -BTN_B = 0x131 -BTN_C = 0x132 -BTN_X = 0x133 -BTN_Y = 0x134 -BTN_Z = 0x135 -BTN_TL = 0x136 -BTN_TR = 0x137 -BTN_TL2 = 0x138 -BTN_TR2 = 0x139 -BTN_SELECT = 0x13a -BTN_START = 0x13b -BTN_MODE = 0x13c -BTN_THUMBL = 0x13d -BTN_THUMBR = 0x13e - -BTN_DIGI = 0x140 -BTN_TOOL_PEN = 0x140 -BTN_TOOL_RUBBER = 0x141 -BTN_TOOL_BRUSH = 0x142 -BTN_TOOL_PENCIL = 0x143 -BTN_TOOL_AIRBRUSH = 0x144 -BTN_TOOL_FINGER = 0x145 -BTN_TOOL_MOUSE = 0x146 -BTN_TOOL_LENS = 0x147 -BTN_TOUCH = 0x14a -BTN_STYLUS = 0x14b -BTN_STYLUS2 = 0x14c -BTN_TOOL_DOUBLETAP = 0x14d -BTN_TOOL_TRIPLETAP = 0x14e - -BTN_WHEEL = 0x150 -BTN_GEAR_DOWN = 0x150 -BTN_GEAR_UP = 0x151 - -KEY_OK = 0x160 -KEY_SELECT = 0x161 -KEY_GOTO = 0x162 -KEY_CLEAR = 0x163 -KEY_POWER2 = 0x164 -KEY_OPTION = 0x165 -KEY_INFO = 0x166 -KEY_TIME = 0x167 -KEY_VENDOR = 0x168 -KEY_ARCHIVE = 0x169 -KEY_PROGRAM = 0x16a -KEY_CHANNEL = 0x16b -KEY_FAVORITES = 0x16c -KEY_EPG = 0x16d -KEY_PVR = 0x16e -KEY_MHP = 0x16f -KEY_LANGUAGE = 0x170 -KEY_TITLE = 0x171 -KEY_SUBTITLE = 0x172 -KEY_ANGLE = 0x173 -KEY_ZOOM = 0x174 -KEY_MODE = 0x175 -KEY_KEYBOARD = 0x176 -KEY_SCREEN = 0x177 -KEY_PC = 0x178 -KEY_TV = 0x179 -KEY_TV2 = 0x17a -KEY_VCR = 0x17b -KEY_VCR2 = 0x17c -KEY_SAT = 0x17d -KEY_SAT2 = 0x17e -KEY_CD = 0x17f -KEY_TAPE = 0x180 -KEY_RADIO = 0x181 -KEY_TUNER = 0x182 -KEY_PLAYER = 0x183 -KEY_TEXT = 0x184 -KEY_DVD = 0x185 -KEY_AUX = 0x186 -KEY_MP3 = 0x187 -KEY_AUDIO = 0x188 -KEY_VIDEO = 0x189 -KEY_DIRECTORY = 0x18a -KEY_LIST = 0x18b -KEY_MEMO = 0x18c -KEY_CALENDAR = 0x18d -KEY_RED = 0x18e -KEY_GREEN = 0x18f -KEY_YELLOW = 0x190 -KEY_BLUE = 0x191 -KEY_CHANNELUP = 0x192 -KEY_CHANNELDOWN = 0x193 -KEY_FIRST = 0x194 -KEY_LAST = 0x195 -KEY_AB = 0x196 -KEY_NEXT = 0x197 -KEY_RESTART = 0x198 -KEY_SLOW = 0x199 -KEY_SHUFFLE = 0x19a -KEY_BREAK = 0x19b -KEY_PREVIOUS = 0x19c -KEY_DIGITS = 0x19d -KEY_TEEN = 0x19e -KEY_TWEN = 0x19f - -KEY_DEL_EOL = 0x1c0 -KEY_DEL_EOS = 0x1c1 -KEY_INS_LINE = 0x1c2 -KEY_DEL_LINE = 0x1c3 - -KEY_FN = 0x1d0 -KEY_FN_ESC = 0x1d1 -KEY_FN_F1 = 0x1d2 -KEY_FN_F2 = 0x1d3 -KEY_FN_F3 = 0x1d4 -KEY_FN_F4 = 0x1d5 -KEY_FN_F5 = 0x1d6 -KEY_FN_F6 = 0x1d7 -KEY_FN_F7 = 0x1d8 -KEY_FN_F8 = 0x1d9 -KEY_FN_F9 = 0x1da -KEY_FN_F10 = 0x1db -KEY_FN_F11 = 0x1dc -KEY_FN_F12 = 0x1dd -KEY_FN_1 = 0x1de -KEY_FN_2 = 0x1df -KEY_FN_D = 0x1e0 -KEY_FN_E = 0x1e1 -KEY_FN_F = 0x1e2 -KEY_FN_S = 0x1e3 -KEY_FN_B = 0x1e4 - -KEY_MAX = 0x1ff diff --git a/inevent/__init__.py b/inevent/__init__.py deleted file mode 100644 index 6ab1928..0000000 --- a/inevent/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -# The inevent Python module was adapted from pi3d.event from the pi3d -# project. -# -# Copyright (c) 2016, Joseph Coffland, Cauldron Development LLC. -# Copyright (c) 2015, Tim Skillman. -# Copyright (c) 2015, Paddy Gaunt. -# Copyright (c) 2015, Tom Ritchford. -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from .InEvent import InEvent -from .JogHandler import JogHandler diff --git a/inevent/ioctl.py b/inevent/ioctl.py deleted file mode 100644 index 6e91b40..0000000 --- a/inevent/ioctl.py +++ /dev/null @@ -1,101 +0,0 @@ -# The inevent Python module was adapted from pi3d.event from the pi3d -# project. -# -# Copyright (c) 2016, Joseph Coffland, Cauldron Development LLC. -# Copyright (c) 2015, Tim Skillman. -# Copyright (c) 2015, Paddy Gaunt. -# Copyright (c) 2015, Tom Ritchford. -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -# -# IOCTL macros -# -# ioctl command encoding: 32 bits total, command in lower 16 bits, -# size of the parameter structure in the lower 14 bits of the -# upper 16 bits. -# -# Encoding the size of the parameter structure in the ioctl request -# is useful for catching programs compiled with old versions -# and to avoid overwriting user space outside the user buffer area. -# The highest 2 bits are reserved for indicating the ``access mode''. -# NOTE: This limits the max parameter size to 16kB - 1 -# -# The following is for compatibility across the various Linux -# platforms. The generic ioctl numbering scheme doesn't really enforce -# a type field. De facto, however, the top 8 bits of the lower 16 -# bits are indeed used as a type field, so we might just as well make -# this explicit here. - -import struct - - -sizeof = struct.calcsize - -_IOC_NRBITS = 8 -_IOC_TYPEBITS = 8 -_IOC_SIZEBITS = 14 -_IOC_DIRBITS = 2 - -_IOC_NRMASK = (1 << _IOC_NRBITS) - 1 -_IOC_TYPEMASK = (1 << _IOC_TYPEBITS) - 1 -_IOC_SIZEMASK = (1 << _IOC_SIZEBITS) - 1 -_IOC_DIRMASK = (1 << _IOC_DIRBITS) - 1 - -_IOC_NRSHIFT = 0 -_IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS -_IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS -_IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS - -_IOC_NONE = 0 -_IOC_WRITE = 1 -_IOC_READ = 2 -_IOC_RW = _IOC_READ | _IOC_WRITE - - -def _IOC(dir, type, nr, size): - return int( - (dir << _IOC_DIRSHIFT) | - (type << _IOC_TYPESHIFT) | - (nr << _IOC_NRSHIFT) | - (size << _IOC_SIZESHIFT)) - -# encode ioctl numbers -def _IO(type, nr): return _IOC(_IOC_NONE, type, nr, 0) -def _IOR(type, nr, fmt): return _IOC(_IOC_READ, type, nr, sizeof(fmt)) -def _IOW(type, nr, fmt): return _IOC(_IOC_WRITE, type, nr, sizeof(fmt)) -def _IOWR(type, nr, fmt): return _IOC(_IOC_RW, type, nr, sizeof(fmt)) -def _IOR_BAD(type, nr, fmt): return _IOC(_IOC_READ, type, nr, sizeof(fmt)) -def _IOW_BAD(type, nr, fmt): return _IOC(_IOC_WRITE, type, nr, sizeof(fmt)) -def _IOWR_BAD(type, nr, fmt): return _IOC(_IOC_RW, type, nr, sizeof(fmt)) - -# decode ioctl numbers -def _IOC_DIR(nr): return (nr >> _IOC_DIRSHIFT) & _IOC_DIRMASK -def _IOC_TYPE(nr): return (nr >> _IOC_TYPESHIFT) & _IOC_TYPEMASK -def _IOC_NR(nr): return (nr >> _IOC_NRSHIFT) & _IOC_NRMASK -def _IOC_SIZE(nr): return (nr >> _IOC_SIZESHIFT) & _IOC_SIZEMASK - -# for drivers/sound files -IOC_IN = _IOC_WRITE << _IOC_DIRSHIFT -IOC_OUT = _IOC_READ << _IOC_DIRSHIFT -IOC_INOUT = _IOC_RW << _IOC_DIRSHIFT -IOCSIZE_MASK = _IOC_SIZEMASK << _IOC_SIZESHIFT -IOCSIZE_SHIFT = _IOC_SIZESHIFT - diff --git a/jogtest.py b/jogtest.py deleted file mode 100755 index c58daa3..0000000 --- a/jogtest.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 - -from inevent import InEvent, JogHandler -from inevent.Constants import * - - -if __name__ == "__main__": - # Load config - config = { - "deadband": 0.1, - "axes": [ABS_X, ABS_Y, ABS_RZ], - "arrows": [ABS_HAT0X, ABS_HAT0Y], - "speed": [0x120, 0x121, 0x122, 0x123], - "activate": [0x124, 0x126, 0x125] - } - - # Listen for input events - events = InEvent(types = "js kbd".split()) - handler = JogHandler(config) - - while not events.key_state("KEY_ESC"): - events.process_events(handler) diff --git a/lcd.py b/lcd.py deleted file mode 100755 index d857766..0000000 --- a/lcd.py +++ /dev/null @@ -1,195 +0,0 @@ -#!/usr/bin/env python3 - -import smbus -import time - - -# Control flags -REG_SELECT_BIT = 1 << 0 -READ_BIT = 1 << 1 -ENABLE_BIT = 1 << 2 -BACKLIGHT_BIT = 1 << 3 - -# Commands -LCD_CLEAR_DISPLAY = 1 << 0 -LCD_RETURN_HOME = 1 << 1 -LCD_ENTRY_MODE_SET = 1 << 2 -LCD_DISPLAY_CONTROL = 1 << 3 -LCD_CURSOR_SHIFT = 1 << 4 -LCD_FUNCTION_SET = 1 << 5 -LCD_SET_CGRAM_ADDR = 1 << 6 -LCD_SET_DDRAM_ADDR = 1 << 7 - -# Entry Mode Set flags -LCD_ENTRY_SHIFT_DISPLAY = 1 << 0 -LCD_ENTRY_SHIFT_INC = 1 << 1 -LCD_ENTRY_SHIFT_DEC = 0 << 1 - -# Display Control flags -LCD_BLINK_ON = 1 << 0 -LCD_BLINK_OFF = 0 << 0 -LCD_CURSOR_ON = 1 << 1 -LCD_CURSOR_OFF = 0 << 1 -LCD_DISPLAY_ON = 1 << 2 -LCD_DISPLAY_OFF = 0 << 2 - -# Cursor Shift flags -LCD_SHIFT_RIGHT = 1 << 2 -LCD_SHIFT_LEFT = 0 << 2 -LCD_SHIFT_DISPLAY = 1 << 3 -LCD_SHIFT_CURSOR = 0 << 3 - -# Function Set flags -LCD_5x11_DOTS = 1 << 2 -LCD_5x8_DOTS = 0 << 2 -LCD_2_LINE = 1 << 3 -LCD_1_LINE = 0 << 3 -LCD_8_BIT_MODE = 1 << 4 -LCD_4_BIT_MODE = 0 << 4 - -# Text justification flags -JUSTIFY_LEFT = 0 -JUSTIFY_RIGHT = 1 -JUSTIFY_CENTER = 2 - - - -class LCD: - def __init__(self, port, addr, height = 4, width = 20): - self.addr = addr - self.height = height - self.width = width - - self.bus = smbus.SMBus(port) - self.backlight = True - - self.reset() - - - def reset(self): - self.clear() - self.write(LCD_FUNCTION_SET | LCD_2_LINE | LCD_5x8_DOTS | - LCD_4_BIT_MODE) - self.write(LCD_DISPLAY_CONTROL | LCD_DISPLAY_ON) - self.write(LCD_ENTRY_MODE_SET | LCD_ENTRY_SHIFT_INC) - - - def write_i2c(self, data): - if self.backlight: data |= BACKLIGHT_BIT - - self.bus.write_byte(self.addr, data) - time.sleep(0.0001) - - - # Write half of a command to LCD - def write_nibble(self, data): - self.write_i2c(data) - - # Strobe - self.write_i2c(data | ENABLE_BIT) - time.sleep(0.0005) - - self.write_i2c(data & ~ENABLE_BIT) - time.sleep(0.0001) - - - # Write an 8-bit command to LCD - def write(self, cmd, flags = 0): - self.write_nibble(flags | (cmd & 0xf0)) - self.write_nibble(flags | ((cmd << 4) & 0xf0)) - - - def set_cursor(self, on, blink): - data = LCD_DISPLAY_CONTROL - - if on: data |= LCD_CURSOR_ON - if blink: data |= LCD_BLINK_ON - - self.write(data) - - - def set_backlight(self, enable): - self.backlight = enable - self.write_i2c(0) - - - def program_char(self, addr, data): - if addr < 0 or 8 <= addr: return - - self.write(LCD_SET_CGRAM_ADDR | (addr << 3)) - for x in data: - self.write(x, REG_SELECT_BIT) - - - def goto(self, x, y): - if x < 0 or self.width <= x or y < 0 or self.height <= y: return - self.write(LCD_SET_DDRAM_ADDR | (0, 64, 20, 84)[y] + int(x)) - - - def text(self, msg): - for c in msg: - self.write(ord(c), REG_SELECT_BIT) - - - def display(self, line, msg, justify = JUSTIFY_LEFT): - if justify == JUSTIFY_RIGHT: x = self.width - len(msg) - elif justify == JUSTIFY_CENTER: x = (self.width - len(msg)) / 2 - else: x = 0 - - if x < 0: x = 0 - - self.goto(x, line) - self.text(msg) - - - def shift(self, count = 1, right = True, display = True): - cmd = LCD_CURSOR_SHIFT - if right: cmd |= LCD_SHIFT_RIGHT - if display: cmd |= LCD_SHIFT_DISPLAY - - for i in range(count): self.write(cmd) - - - # Clear LCD and move cursor home - def clear(self): - self.write(LCD_CLEAR_DISPLAY) - self.write(LCD_RETURN_HOME) - - - -if __name__ == "__main__": - lcd = LCD(1, 0x27) - - lcd.clear() - - lcd.program_char(0, (0b11011, - 0b11011, - 0b00000, - 0b01100, - 0b01100, - 0b00000, - 0b11011, - 0b11011)) - - lcd.program_char(1, (0b11000, - 0b01100, - 0b00110, - 0b00011, - 0b00011, - 0b00110, - 0b01100, - 0b11000)) - - lcd.program_char(2, (0b00011, - 0b00110, - 0b01100, - 0b11000, - 0b11000, - 0b01100, - 0b00110, - 0b00011)) - - lcd.display(0, '\0' * lcd.width) - lcd.display(1, 'Hello world!', JUSTIFY_CENTER) - lcd.display(2, '\1\2' * (lcd.width / 2)) - lcd.display(3, '12345678901234567890') diff --git a/scripts/bbctrl.init.d b/scripts/bbctrl.init.d new file mode 100644 index 0000000..b7a0fb6 --- /dev/null +++ b/scripts/bbctrl.init.d @@ -0,0 +1,51 @@ +#!/bin/bash + +### BEGIN INIT INFO +# Provides: bbctl +# Required-Start: $local_fs $network +# Required-Stop: $local_fs $network +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Buildbotics Controller Web service +# Description: Buildbotics Controller Web service +### END INIT INFO + +DAEMON=/home/bbmc/bbctrl.py +DAEMON_NAME=bbctrl +DAEMON_OPTS="" +DAEMON_USER=root +DAEMON_DIR=$(dirname $DAEMON) +PIDFILE=/var/run/$DAEMON_NAME.pid + +. /lib/lsb/init-functions + + +do_start () { + log_daemon_msg "Starting system $DAEMON_NAME daemon" + 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" + log_end_msg $? +} + + +do_stop () { + log_daemon_msg "Stopping system $DAEMON_NAME daemon" + start-stop-daemon --stop --pidfile $PIDFILE --retry 10 + log_end_msg $? +} + + +case "$1" in + start|stop) do_${1} ;; + restart|reload|force-reload) do_stop; do_start ;; + status) + status_of_proc "$DAEMON_NAME" "$DAEMON" && exit 0 || exit $? + ;; + + *) + echo "Usage: /etc/init.d/$DAEMON_NAME {start|stop|restart|status}" + exit 1 + ;; +esac diff --git a/scripts/jogtest.py b/scripts/jogtest.py new file mode 100755 index 0000000..c58daa3 --- /dev/null +++ b/scripts/jogtest.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +from inevent import InEvent, JogHandler +from inevent.Constants import * + + +if __name__ == "__main__": + # Load config + config = { + "deadband": 0.1, + "axes": [ABS_X, ABS_Y, ABS_RZ], + "arrows": [ABS_HAT0X, ABS_HAT0Y], + "speed": [0x120, 0x121, 0x122, 0x123], + "activate": [0x124, 0x126, 0x125] + } + + # Listen for input events + events = InEvent(types = "js kbd".split()) + handler = JogHandler(config) + + while not events.key_state("KEY_ESC"): + events.process_events(handler) diff --git a/scripts/setup_rpi.sh b/scripts/setup_rpi.sh new file mode 100755 index 0000000..dd38975 --- /dev/null +++ b/scripts/setup_rpi.sh @@ -0,0 +1,97 @@ +#!/bin/bash + +ID=2 + +# Update the system +apt-get update +apt-get dist-upgrade -y + +# Resize FS +# TODO no /dev/root in Jessie +ROOT_PART=$(readlink /dev/root) +PART_NUM=${ROOT_PART#mmcblk0p} + +if [ "$PART_NUM" != "$ROOT_PART" ]; then + # Get the starting offset of the root partition + PART_START=$( + parted /dev/mmcblk0 -ms unit s p | grep "^${PART_NUM}" | cut -f 2 -d:) + [ "$PART_START" ] && + + fdisk /dev/mmcblk0 </etc/init.d/resize2fs_once && +#!/bin/sh +### BEGIN INIT INFO +# Provides: resize2fs_once +# Required-Start: +# Required-Stop: +# Default-Start: 2 3 4 5 S +# Default-Stop: +# Short-Description: Resize the root filesystem to fill partition +# Description: +### END INIT INFO +. /lib/lsb/init-functions +case "$1" in + start) + log_daemon_msg "Starting resize2fs_once" && + resize2fs /dev/root && + rm /etc/init.d/resize2fs_once && + update-rc.d resize2fs_once remove && + log_end_msg $? + ;; + *) + echo "Usage: $0 start" >&2 + exit 3 + ;; +esac +EOF + + chmod +x /etc/init.d/resize2fs_once && + update-rc.d resize2fs_once defaults +fi + +# Install pacakges +apt-get install -y avahi-daemon avrdude minicom python3-pip i2c-tools +pip-3.2 install tornado sockjs-tornado pyserial smbus + +# Clean +apt-get autoclean + +# Change hostname +sed -i "s/raspberrypi/bbctrl$ID/" /etc/hosts /etc/hostname + +# Create user +useradd -m -p $(openssl passwd -1 buildbotics) -s /bin/bash bbmc +echo "bbmc ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers + +# Disable console on serial port +#sed -i 's/^\(.*ttyAMA0.*\)$/# \1/' /etc/inittab +sed -i 's/console=ttyAMA0,115200 //' /boot/cmdline.txt + +# Disable extra gettys +sed -i 's/^\([23456]:.*\/sbin\/getty\)/#\1/' /etc/inittab + +# Enable I2C +sed -i 's/#dtparam=i2c/dtparam=i2c/' /boot/config.txt +echo i2c-bcm2708 >> /etc/modules +echo i2c-dev >> /etc/modules + +# Install bbctrl w/ init.d script +cp bbctrl.init.d /etc/init.d/bbctrl +chmod +x /etc/init.d/bbctrl +update-rc.d bbctrl defaults + +# TODO setup input and serial device permissions in udev + +reboot diff --git a/scripts/svg2abs.js b/scripts/svg2abs.js new file mode 100644 index 0000000..7cfc6dd --- /dev/null +++ b/scripts/svg2abs.js @@ -0,0 +1,64 @@ +function convertToAbsolute(path) { + var x0, y0, x1, y1, x2, y2, segs = path.pathSegList; + + for (var x = 0, y = 0, i = 0, len = segs.numberOfItems; i < len; i++) { + var seg = segs.getItem(i), c = seg.pathSegTypeAsLetter; + + if (/[MLHVCSQTA]/.test(c)){ + if ('x' in seg) x = seg.x; + if ('y' in seg) y = seg.y; + + } else { + if ('x1' in seg) x1 = x + seg.x1; + if ('x2' in seg) x2 = x + seg.x2; + if ('y1' in seg) y1 = y + seg.y1; + if ('y2' in seg) y2 = y + seg.y2; + if ('x' in seg) x += seg.x; + if ('y' in seg) y += seg.y; + + switch(c) { + case 'm': + segs.replaceItem(path.createSVGPathSegMovetoAbs(x, y), i); + break; + case 'l': + segs.replaceItem(path.createSVGPathSegLinetoAbs(x, y), i); + break; + case 'h': + segs.replaceItem(path.createSVGPathSegLinetoHorizontalAbs(x), i); + break; + case 'v': + segs.replaceItem(path.createSVGPathSegLinetoVerticalAbs(y), i); + break; + case 'c': + segs.replaceItem( + path.createSVGPathSegCurvetoCubicAbs(x, y, x1, y1, x2, y2), i); + break; + case 's': + segs.replaceItem( + path.createSVGPathSegCurvetoCubicSmoothAbs(x, y, x2, y2), i); + break; + case 'q': + segs.replaceItem( + path.createSVGPathSegCurvetoQuadraticAbs(x, y, x1, y1), i); + break; + case 't': + segs.replaceItem( + path.createSVGPathSegCurvetoQuadraticSmoothAbs(x, y), i); + break; + case 'a': + segs.replaceItem( + path.createSVGPathSegArcAbs(x, y, seg.r1, seg.r2, seg.angle, + seg.largeArcFlag, seg.sweepFlag), i); + break; + + case 'z': case 'Z': + x = x0; + y = y0; + break; + } + } + + // Record the start of a subpath + if (c == 'M' || c == 'm') x0 = x, y0 = y; + } +} diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..f0a591e --- /dev/null +++ b/setup.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +from setuptools import setup + +setup( + name = 'bbctrl', + version = '0.0.2', + description = 'Buildbotics Machine Controller', + long_description = open('README.md', 'rt').read(), + author = 'Joseph Coffland', + author_email = 'joseph@buildbotics.org', + platforms = ['any'], + license = 'GPL 3+', + url = 'https://github.com/buildbotics/rpi-firmware', + package_dir = {'': 'src/py'}, + packages = ['bbctrl', 'inevent', 'lcd'], + include_package_data = True, + eager_resources = ['bbctrl/http/*'], + entry_points = { + 'console_scripts': [ + 'bbctrl = bbctrl:run' + ] + }, + install_requires = 'tornado sockjs-tornado pyserial smbus2'.split(), + zip_save = False, + ) diff --git a/setup_rpi.sh b/setup_rpi.sh deleted file mode 100755 index dd38975..0000000 --- a/setup_rpi.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/bin/bash - -ID=2 - -# Update the system -apt-get update -apt-get dist-upgrade -y - -# Resize FS -# TODO no /dev/root in Jessie -ROOT_PART=$(readlink /dev/root) -PART_NUM=${ROOT_PART#mmcblk0p} - -if [ "$PART_NUM" != "$ROOT_PART" ]; then - # Get the starting offset of the root partition - PART_START=$( - parted /dev/mmcblk0 -ms unit s p | grep "^${PART_NUM}" | cut -f 2 -d:) - [ "$PART_START" ] && - - fdisk /dev/mmcblk0 </etc/init.d/resize2fs_once && -#!/bin/sh -### BEGIN INIT INFO -# Provides: resize2fs_once -# Required-Start: -# Required-Stop: -# Default-Start: 2 3 4 5 S -# Default-Stop: -# Short-Description: Resize the root filesystem to fill partition -# Description: -### END INIT INFO -. /lib/lsb/init-functions -case "$1" in - start) - log_daemon_msg "Starting resize2fs_once" && - resize2fs /dev/root && - rm /etc/init.d/resize2fs_once && - update-rc.d resize2fs_once remove && - log_end_msg $? - ;; - *) - echo "Usage: $0 start" >&2 - exit 3 - ;; -esac -EOF - - chmod +x /etc/init.d/resize2fs_once && - update-rc.d resize2fs_once defaults -fi - -# Install pacakges -apt-get install -y avahi-daemon avrdude minicom python3-pip i2c-tools -pip-3.2 install tornado sockjs-tornado pyserial smbus - -# Clean -apt-get autoclean - -# Change hostname -sed -i "s/raspberrypi/bbctrl$ID/" /etc/hosts /etc/hostname - -# Create user -useradd -m -p $(openssl passwd -1 buildbotics) -s /bin/bash bbmc -echo "bbmc ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers - -# Disable console on serial port -#sed -i 's/^\(.*ttyAMA0.*\)$/# \1/' /etc/inittab -sed -i 's/console=ttyAMA0,115200 //' /boot/cmdline.txt - -# Disable extra gettys -sed -i 's/^\([23456]:.*\/sbin\/getty\)/#\1/' /etc/inittab - -# Enable I2C -sed -i 's/#dtparam=i2c/dtparam=i2c/' /boot/config.txt -echo i2c-bcm2708 >> /etc/modules -echo i2c-dev >> /etc/modules - -# Install bbctrl w/ init.d script -cp bbctrl.init.d /etc/init.d/bbctrl -chmod +x /etc/init.d/bbctrl -update-rc.d bbctrl defaults - -# TODO setup input and serial device permissions in udev - -reboot diff --git a/splash.py b/splash.py deleted file mode 100755 index d4277c1..0000000 --- a/splash.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python3 - -import lcd - -if __name__ == "__main__": - screen = lcd.LCD(1, 0x27) - - screen.clear() - screen.display(0, 'Buildbotics', lcd.JUSTIFY_CENTER) - screen.display(1, 'Controller', lcd.JUSTIFY_CENTER) - screen.display(3, 'Booting...', lcd.JUSTIFY_CENTER) diff --git a/src/py/bbctrl/__init__.py b/src/py/bbctrl/__init__.py new file mode 100755 index 0000000..25c5caf --- /dev/null +++ b/src/py/bbctrl/__init__.py @@ -0,0 +1,402 @@ +#!/usr/bin/env python3 + +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 argparse + +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() + + +def get_resource(path): + return resource_filename(Requirement.parse('bbctrl'), 'bbctrl/' + path) + + +def on_exit(sig, func = None): + print('exit handler triggered') + 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('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, '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') + + parser.add_argument('-p', '--port', default = 80, + type = int, help = 'HTTP port') + parser.add_argument('-a', '--addr', metavar = 'IP', default = '0.0.0.0', + help = 'HTTP address to bind') + parser.add_argument('-s', '--serial', default = '/dev/ttyAMA0', + help = 'Serial device') + parser.add_argument('-b', '--baud', default = 115200, type = int, + help = 'Serial baud rate') + parser.add_argument('--lcd-port', default = 1, type = int, + help = 'LCD I2C port') + parser.add_argument('--lcd-addr', default = 0x27, type = int, + help = 'LCD I2C address') + parser.add_argument('-v', '--verbose', action = 'store_true', + help = 'Verbose output') + + 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) + + # 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() + + # Setup LCD + global screen + screen = lcd.LCD(args.lcd_port, args.lcd_addr) + splash(screen) + atexit.register(goodbye, screen) + + # Start the web server + app = web.Application(router.urls + handlers) + + 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) + + print('Listening on http://{}:{}/'.format(args.addr, args.port)) + + ioloop.IOLoop.instance().start() + + +if __name__ == "__main__": run() diff --git a/src/py/bbctrl/http b/src/py/bbctrl/http new file mode 120000 index 0000000..de7005b --- /dev/null +++ b/src/py/bbctrl/http @@ -0,0 +1 @@ +../../../build/http/ \ No newline at end of file diff --git a/src/py/inevent/AbsAxisScaling.py b/src/py/inevent/AbsAxisScaling.py new file mode 100644 index 0000000..bd4baa1 --- /dev/null +++ b/src/py/inevent/AbsAxisScaling.py @@ -0,0 +1,80 @@ +# The inevent Python module was adapted from pi3d.event from the pi3d +# project. +# +# Copyright (c) 2016, Joseph Coffland, Cauldron Development LLC. +# Copyright (c) 2015, Tim Skillman. +# Copyright (c) 2015, Paddy Gaunt. +# Copyright (c) 2015, Tom Ritchford. +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import array +import fcntl +import struct +from inevent import ioctl + + +def EVIOCGABS(axis): + return ioctl._IOR(ord('E'), 0x40 + axis, "ffffff") # get abs value/limits + + + +class AbsAxisScaling(object): + """ + Fetches and implements the EV_ABS axis scaling. + + The constructor fetches the scaling values from the given stream for the + given axis using an ioctl. + + There is a scale method, which scales a given value to the range -1..+1. + """ + + def __init__(self, stream, axis): + """ + Fetch the scale values for this stream and fill in the instance + variables accordingly. + """ + s = array.array("i", [1, 2, 3, 4, 5, 6]) + try: + fcntl.ioctl(stream.filehandle, EVIOCGABS(axis), s) + + except IOError: + self.value = self.minimum = self.maximum = self.fuzz = self.flat = \ + self.resolution = 1 + + else: + self.value, self.minimum, self.maximum, self.fuzz, self.flat, \ + self.resolution = struct.unpack("iiiiii", s) + + + def __str__(self): + return "Value {0} Min {1}, Max {2}, Fuzz {3}, Flat {4}, Res {5}".format( + self.value, self.minimum, self.maximum, self.fuzz, self.flat, + self.resolution) + + + def scale(self, value): + """ + scales the given value into the range -1..+1 + """ + return (float(value) - float(self.minimum)) / \ + float(self.maximum - self.minimum) * 2.0 - 1.0 + diff --git a/src/py/inevent/Constants.py b/src/py/inevent/Constants.py new file mode 100644 index 0000000..00be4cb --- /dev/null +++ b/src/py/inevent/Constants.py @@ -0,0 +1,97 @@ +# The inevent Python module was adapted from pi3d.event from the pi3d +# project. +# +# Copyright (c) 2016, Joseph Coffland, Cauldron Development LLC. +# Copyright (c) 2015, Tim Skillman. +# Copyright (c) 2015, Paddy Gaunt. +# Copyright (c) 2015, Tom Ritchford. +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +EV_SYN = 0x00 +EV_KEY = 0x01 +EV_REL = 0x02 +EV_ABS = 0x03 +EV_MSC = 0x04 +EV_SW = 0x05 +EV_LED = 0x11 +EV_SND = 0x12 +EV_REP = 0x14 +EV_FF = 0x15 +EV_PWR = 0x16 +EV_FF_STATUS = 0x17 + +ev_type_name = {} +ev_type_name[EV_SYN] = "SYN" +ev_type_name[EV_KEY] = "KEY" +ev_type_name[EV_REL] = "REL" +ev_type_name[EV_ABS] = "ABS" +ev_type_name[EV_MSC] = "MSC" +ev_type_name[EV_SW] = "SW" +ev_type_name[EV_LED] = "LED" +ev_type_name[EV_SND] = "SND" +ev_type_name[EV_REP] = "REP" +ev_type_name[EV_FF] = "FF" +ev_type_name[EV_PWR] = "PWR" +ev_type_name[EV_FF_STATUS] = "FF_STATUS" + +SYN_REPORT = 0 +SYN_CONFIG = 1 + +REL_X = 0x00 +REL_Y = 0x01 +REL_Z = 0x02 +REL_RX = 0x03 +REL_RY = 0x04 +REL_RZ = 0x05 +REL_HWHEEL = 0x06 +REL_DIAL = 0x07 +REL_WHEEL = 0x08 +REL_MISC = 0x09 +REL_MAX = 0x0f + +ABS_X = 0x00 +ABS_Y = 0x01 +ABS_Z = 0x02 +ABS_RX = 0x03 +ABS_RY = 0x04 +ABS_RZ = 0x05 +ABS_THROTTLE = 0x06 +ABS_RUDDER = 0x07 +ABS_WHEEL = 0x08 +ABS_GAS = 0x09 +ABS_BRAKE = 0x0a +ABS_HAT0X = 0x10 +ABS_HAT0Y = 0x11 +ABS_HAT1X = 0x12 +ABS_HAT1Y = 0x13 +ABS_HAT2X = 0x14 +ABS_HAT2Y = 0x15 +ABS_HAT3X = 0x16 +ABS_HAT3Y = 0x17 +ABS_PRESSURE = 0x18 +ABS_DISTANCE = 0x19 +ABS_TILT_X = 0x1a +ABS_TILT_Y = 0x1b +ABS_TOOL_WIDTH = 0x1c +ABS_VOLUME = 0x20 +ABS_MISC = 0x28 +ABS_MAX = 0x3f diff --git a/src/py/inevent/Event.py b/src/py/inevent/Event.py new file mode 100644 index 0000000..c14e9d4 --- /dev/null +++ b/src/py/inevent/Event.py @@ -0,0 +1,115 @@ +# The inevent Python module was adapted from pi3d.event from the pi3d +# project. +# +# Copyright (c) 2016, Joseph Coffland, Cauldron Development LLC. +# Copyright (c) 2015, Tim Skillman. +# Copyright (c) 2015, Paddy Gaunt. +# Copyright (c) 2015, Tom Ritchford. +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import struct + +from inevent import Format +from inevent.Constants import * + + + +class Event(object): + """ + A single event from the linux input event system. + + Events are tuples: (Time, Type, Code, Value) + In addition we remember the stream it came from. + + Externally, only the unhandled event handler gets passed the whole event, + but the SYN handler gets the code and value. (Also the keyboard handler, but + those are renamed to key and value.) + + This class is responsible for converting the Linux input event structure into + one of these objects and back again. + """ + def __init__(self, stream, time = None, type = None, code = None, + value = None): + """ + Create a new event. + + Generally all but the stream parameter are left out; we will want to + populate the object from a Linux input event using decode. + """ + self.stream = stream + self.time = time + self.type = type + self.code = code + self.value = value + + + def get_type_name(self): + if self.type not in ev_type_name: return '0x%x' % self.type + return ev_type_name[self.type] + + + def get_source(self): + return "%s[%d]" % (self.stream.devType, self.stream.devIndex) + + + def __str__(self): + """ + Uses the stream to give the device type and whether it is currently grabbed. + """ + grabbed = "grabbed" if self.stream.grabbed else "ungrabbed" + + return "Event %s %s @%f: %s 0x%x=0x%x" % ( + self.get_source(), grabbed, self.time, self.get_type_name(), self.code, + self.value) + + + def __repr__(self): + return "Event(%s, %f, 0x%x, 0x%x, 0x%x)" % ( + repr(self.stream), self.time, self.type, self.code, self.value) + + + def encode(self): + """ + Encode this event into a Linux input event structure. + + The output is packed into a string. It is unlikely that this function + will be required, but it might as well be here. + """ + tint = long(self.time) + tfrac = long((self.time - tint) * 1000000) + + return \ + struct.pack(Format.Event, tsec, tfrac, self.type, self.code, self.value) + + + def decode(self, s): + """ + Decode a Linux input event into the fields of this object. + + Arguments: + *s* + A binary structure packed into a string. + """ + tsec, tfrac, self.type, self.code, self.value = \ + struct.unpack(Format.Event, s) + + self.time = tsec + tfrac / 1000000.0 diff --git a/src/py/inevent/EventHandler.py b/src/py/inevent/EventHandler.py new file mode 100644 index 0000000..97642503 --- /dev/null +++ b/src/py/inevent/EventHandler.py @@ -0,0 +1,119 @@ +# The inevent Python module was adapted from pi3d.event from the pi3d +# project. +# +# Copyright (c) 2016, Joseph Coffland, Cauldron Development LLC. +# Copyright (c) 2015, Tim Skillman. +# Copyright (c) 2015, Paddy Gaunt. +# Copyright (c) 2015, Tom Ritchford. +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from inevent.Constants import * +from inevent.EventStream import EventStream + + +class EventHandler(object): + """ + A class to handle events. + + Four types of events are handled: REL (mouse movement), KEY (keybaord keys and + other device buttons), ABS (joysticks and gamepad analogue sticks) and SYN + (delimits simultaneous events such as mouse movements) + """ + def __init__(self): + self.buttons = dict() + + + def event(self, event, handler): + """ + Handles the given event. + + If the event is passed to a handler or otherwise handled then returns None, + else returns the event. All handlers are optional. + + All key events are handled by putting them in the self.buttons dict, and + optionally by calling the supplied handler. + + REL X, Y and wheel V and H events are all accumulated internally and + also optionally passed to the supplied handler. All these events are + handled. + + ABS X, Y, Z, RX, RY, RZ, Hat0X, Hat0Y are all accumulated internally and + also optionally passed to the supplied handler. Other ABS events are not + handled. + + All SYN events are passed to the supplied handler. + + There are several ABS events that we do not handle. In particular: + THROTTLE, RUDDER, WHEEL, GAS, BRAKE, HAT1, HAT2, HAT3, PRESSURE, + DISTANCE, TILT, TOOL_WIDTH. Implementing these is left as an exercise + for the interested reader. + + Likewise, since one handler is handling all events for all devices, we + may get the situation where two devices return the same button. The only + way to handle that would seem to be to have a key dict for every device, + which seems needlessly profligate for a situation that may never arise. + """ + + state = event.stream.state + + if event.type == EV_KEY: self.buttons[event.code] = event.value + elif event.type == EV_REL: state.rel[event.code] += event.value + elif event.type == EV_ABS: + state.abs[event.code] = event.stream.scale(event.code, event.value) + + if handler: handler(event, state) + + + def key_state(self, code): + """ + Returns the last event value for the given key code. + + Key names can be converted to key codes using codeOf[str]. + If the key is pressed the returned value will be 1 (pressed) or 2 (held). + If the key is not pressed, the returned value will be 0. + """ + return self.buttons.get(code, 0) + + + def clear_key(self, code): + """ + Clears the event value for the given key code. + + Key names can be converted to key codes using codeOf[str]. + This emulates a key-up but does not generate any events. + """ + self.buttons[code] = 0 + + + def get_keys(self): + """ + Returns the first of whichever keys have been pressed. + + Key names can be converted to key codes using codeOf[str]. + This emulates a key-up but does not generate any events. + """ + k_list = [] + + for k in self.buttons: + if self.buttons[k] != 0: k_list.append(k) + + return k_list diff --git a/src/py/inevent/EventState.py b/src/py/inevent/EventState.py new file mode 100644 index 0000000..a87ae7b --- /dev/null +++ b/src/py/inevent/EventState.py @@ -0,0 +1,118 @@ +# The inevent Python module was adapted from pi3d.event from the pi3d +# project. +# +# Copyright (c) 2016, Joseph Coffland, Cauldron Development LLC. +# Copyright (c) 2015, Tim Skillman. +# Copyright (c) 2015, Paddy Gaunt. +# Copyright (c) 2015, Tom Ritchford. +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from inevent.Constants import * + + +class EventState: + def __init__(self): + self.abs = [0.0] * ABS_MAX + self.rel = [0.0] * REL_MAX + + + def __str__(self): + return ("({:6.3f}, {:6.3f}, {:6.3f}) ".format(*self.get_joystick3d()) + + "({:6.3f}, {:6.3f}, {:6.3f}) ".format(*self.get_joystickR3d()) + + "({:2.0f}, {:2.0f}) ".format(*self.get_hat()) + + "({:d}, {:d}) ".format(*self.get_mouse()) + + "({:d}, {:d})".format(*self.get_wheel())) + + + def get_joystick(self): + """ + Returns the x,y coordinates for a joystick or left gamepad analogue stick. + + The values are returned as a tuple. All values are -1.0 to +1.0 with + 0.0 being centred. + """ + return self.abs[ABS_X], self.abs[ABS_Y] + + + def get_joystick3d(self): + """ + Returns the x,y,z coordinates for a joystick or left gamepad analogue stick + + The values are returned as a tuple. All values are -1.0 to +1.0 with + 0.0 being centred. + """ + return self.abs[ABS_X], self.abs[ABS_Y], self.abs[ABS_Z] + + + def get_joystickR(self): + """ + Returns the x,y coordinates for a right gamepad analogue stick. + + The values are returned as a tuple. For some odd reason, the gamepad + returns values in the Z axes of both joysticks, with y being the first. + + All values are -1.0 to +1.0 with 0.0 being centred. + """ + return self.abs[ABS_RZ], self.abs[ABS_Z] + + + def get_joystickR3d(self): + """ + Returns the x,y,z coordinates for a 2nd joystick control + + The values are returned as a tuple. All values are -1.0 to +1.0 with + 0.0 being centred. + """ + return self.abs[ABS_RX], self.abs[ABS_RY], self.abs[ABS_RZ] + + + def get_hat(self): + """ + Returns the x,y coordinates for a joystick hat or gamepad direction pad + + The values are returned as a tuple. All values are -1.0 to +1.0 with + 0.0 being centred. + """ + return self.abs[ABS_HAT0X], self.abs[ABS_HAT0Y] + + + def get_mouse(self): + return self.rel[REL_X], self.rel[REL_Y] + + + def get_wheel(self): + return self.rel[REL_WHEEL], self.rel[REL_HWHEEL] + + + def get_mouse_movement(self): + """ + Returns the accumulated REL (mouse or other relative device) movements + since the last call. + + The returned value is a tuple: (X, Y, WHEEL, H-WHEEL) + """ + ret = self.get_mouse() + self.get_wheel() + + self.rel[REL_X] = self.rel[REL_Y] = 0 + self.rel[REL_WHEEL] = self.rel[REL_HWHEEL] = 0 + + return ret diff --git a/src/py/inevent/EventStream.py b/src/py/inevent/EventStream.py new file mode 100644 index 0000000..408e5ec --- /dev/null +++ b/src/py/inevent/EventStream.py @@ -0,0 +1,184 @@ +# The inevent Python module was adapted from pi3d.event from the pi3d +# project. +# +# Copyright (c) 2016, Joseph Coffland, Cauldron Development LLC. +# Copyright (c) 2015, Tim Skillman. +# Copyright (c) 2015, Paddy Gaunt. +# Copyright (c) 2015, Tom Ritchford. +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import fcntl +import os +import select + +from inevent.Constants import * +from inevent import ioctl +from inevent.AbsAxisScaling import AbsAxisScaling +from inevent import Event +from inevent import Format +from inevent.EventState import EventState + + +EVIOCGRAB = ioctl._IOW(ord('E'), 0x90, "i") # Grab/Release device + + + +class EventStream(object): + """ + encapsulates the event* file handling + + Each device is represented by a file in /dev/input called eventN, where N is + a small number. (Actually, a keybaord can be represented by two such files.) + Instances of this class open one of these files and provide means to read + events from them. + + Class methods also exist to read from multiple files simultaneously, and + also to grab and ungrab all instances of a given type. + """ + axisX = 0 + axisY = 1 + axisZ = 2 + axisRX = 3 + axisRY = 4 + axisRZ = 5 + axisHat0X = 6 + axisHat0Y = 7 + axisHat1X = 8 + axisHat1Y = 9 + axisHat2X = 10 + axisHat2Y = 11 + axisHat3X = 12 + axisHat3Y = 13 + axisThrottle = 14 + axisRudder = 15 + axisWheel = 16 + axisGas = 17 + axisBrake = 18 + axisPressure = 19 + axisDistance = 20 + axisTiltX = 21 + axisTiltY = 22 + axisToolWidth = 23 + numAxes = 24 + + axisToEvent = [ + ABS_X, ABS_Y, ABS_Z, ABS_RX, ABS_RY, ABS_RZ, ABS_HAT0X, ABS_HAT0Y, + ABS_HAT1X, ABS_HAT1Y, ABS_HAT2X, ABS_HAT2Y, ABS_HAT3X, ABS_HAT3Y, + ABS_THROTTLE, ABS_RUDDER, ABS_WHEEL, ABS_GAS, ABS_BRAKE, ABS_PRESSURE, + ABS_DISTANCE, ABS_TILT_X, ABS_TILT_Y, ABS_TOOL_WIDTH] + + + def __init__(self, devIndex, devType): + """ + Opens the given /dev/input/event file and grabs it. + + Also adds it to a class-global list of all existing streams. + """ + self.devIndex = devIndex + self.devType = devType + self.filename = "/dev/input/event" + str(devIndex) + self.filehandle = os.open(self.filename, os.O_RDWR) + self.state = EventState() + self.grab(True) + self.absInfo = [None] * ABS_MAX + + if devType == "js": + for axis in range(ABS_MAX): + self.absInfo[axis] = AbsAxisScaling(self, axis) + + + def scale(self, axis, value): + """ + Scale the given value according to the given axis. + + acquire_abs_info must have been previously called to acquire the data to + do the scaling. + """ + assert axis < ABS_MAX, "Axis number out of range" + + if self.absInfo[axis]: return self.absInfo[axis].scale(value) + else: return value + + + def grab(self, grab = True): + """ + Grab (or release) exclusive access to all devices of the given type. + + The devices are grabbed if grab is True and released if grab is False. + + All devices are grabbed to begin with. We might want to ungrab the + keyboard for example to use it for text entry. While not grabbed, all + key-down and key-hold events are filtered out. + """ + fcntl.ioctl(self.filehandle, EVIOCGRAB, 1 if grab else 0) + self.grabbed = grab + + + def __iter__(self): + """s + Required to make this class an iterator + """ + return self + + + def next(self): + """ + Returns the next waiting event. + + If no event is waiting, returns None. + """ + ready = select.select([self.filehandle], [], [], 0)[0] + if ready: return self.read() + + + def read(self): + """ + Read and return the next waiting event. + """ + try: + s = os.read(self.filehandle, Format.EventSize) + if s: + event = Event.Event(self) + event.decode(s) + return event + + except: pass + + + def __enter__(self): return self + + + def release(self): + "Ungrabs the file and closes it." + + try: + self.grab(False) + os.close(self.filehandle) + + except: + pass + + + def __exit__(self, type, value, traceback): + "Ungrabs the file and closes it." + + self.release() diff --git a/src/py/inevent/FindDevices.py b/src/py/inevent/FindDevices.py new file mode 100644 index 0000000..17a667a --- /dev/null +++ b/src/py/inevent/FindDevices.py @@ -0,0 +1,215 @@ +# The inevent Python module was adapted from pi3d.event from the pi3d +# project. +# +# Copyright (c) 2016, Joseph Coffland, Cauldron Development LLC. +# Copyright (c) 2015, Tim Skillman. +# Copyright (c) 2015, Paddy Gaunt. +# Copyright (c) 2015, Tom Ritchford. +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import re +from inevent.Constants import * + + +def test_bit(nlst, b): + index = b / 32 + bit = b % 32 + return index < len(nlst) and nlst[index] & (1 << bit) + + +def EvToStr(events): + s = [] + + if test_bit(events, EV_SYN): s.append("EV_SYN") + if test_bit(events, EV_KEY): s.append("EV_KEY") + if test_bit(events, EV_REL): s.append("EV_REL") + if test_bit(events, EV_ABS): s.append("EV_ABS") + if test_bit(events, EV_MSC): s.append("EV_MSC") + if test_bit(events, EV_LED): s.append("EV_LED") + if test_bit(events, EV_SND): s.append("EV_SND") + if test_bit(events, EV_REP): s.append("EV_REP") + 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 + + +class DeviceCapabilities(object): + def __init__(self, firstLine, filehandle): + self.EV_SYNevents = [] + self.EV_KEYevents = [] + self.EV_RELevents = [] + self.EV_ABSevents = [] + self.EV_MSCevents = [] + self.EV_LEDevents = [] + self.EV_SNDevents = [] + self.EV_REPevents = [] + self.EV_FFevents = [] + 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) + self.bus = 0 + self.vendor = 0 + self.product = 0 + self.version = 0 + + else: + self.bus = int(match.group(1), base = 16) + self.vendor = int(match.group(2), base = 16) + self.product = int(match.group(3), base = 16) + self.version = int(match.group(4), base = 16) + + for line in filehandle: + if not line.strip(): break + + if line[0] == "N": + match = re.search('Name="([^"]+)"', line) + if match: self.name = match.group(1) + else: self.name = "UNKNOWN" + + elif line[0] == "P": + match = re.search('Phys=(.+)', line) + if match: self.phys = match.group(1) + else: self.phys = "UNKNOWN" + + elif line[0] == "S": + match = re.search('Sysfs=(.+)', line) + if match: self.sysfs = match.group(1) + else: self.sysfs = "UNKNOWN" + + elif line[0] == "U": + match = re.search('Uniq=(.*)', line) + if match: self.uniq = match.group(1) + else: self.uniq = "UNKNOWN" + + elif line[0] == "H": + match = re.search('Handlers=(.+)', line) + if match: self.handlers = match.group(1).split() + else: self.handlers = [] + + elif line[:5] == "B: EV": + eventsNums = [int(x, base = 16) for x in line[6:].split()] + eventsNums.reverse() + self.eventTypes = eventsNums + + elif line[:6] == "B: KEY": + eventsNums = [int(x, base = 16) for x in line[7:].split()] + eventsNums.reverse() + self.EV_KEYevents = eventsNums + + elif line[:6] == "B: ABS": + eventsNums = [int(x, base = 16) for x in line[7:].split()] + eventsNums.reverse() + self.EV_ABSevents = eventsNums + + elif line[:6] == "B: MSC": + eventsNums = [int(x, base = 16) for x in line[7:].split()] + eventsNums.reverse() + self.EV_MSCevents = eventsNums + + elif line[:6] == "B: REL": + eventsNums = [int(x, base = 16) for x in line[7:].split()] + eventsNums.reverse() + self.EV_RELevents = eventsNums + + elif line[:6] == "B: LED": + eventsNums = [int(x, base = 16) for x in line[7:].split()] + eventsNums.reverse() + self.EV_LEDevents = eventsNums + + for handler in self.handlers: + if handler[:5] == "event": self.eventIndex = int(handler[5:]) + + self.isMouse = False + self.isKeyboard = False + self.isJoystick = False + + + def doesProduce(self, eventType, eventCode): + return test_bit(self.eventTypes, eventType) and ( + (eventType == EV_SYN and test_bit(self.EV_SYNevents, eventCode)) or + (eventType == EV_KEY and test_bit(self.EV_KEYevents, eventCode)) or + (eventType == EV_REL and test_bit(self.EV_RELevents, eventCode)) or + (eventType == EV_ABS and test_bit(self.EV_ABSevents, eventCode)) or + (eventType == EV_MSC and test_bit(self.EV_MSCevents, eventCode)) or + (eventType == EV_LED and test_bit(self.EV_LEDevents, eventCode)) or + (eventType == EV_SND and test_bit(self.EV_SNDevents, eventCode)) or + (eventType == EV_REP and test_bit(self.EV_REPevents, eventCode)) or + (eventType == EV_FF and test_bit(self.EV_FFevents, eventCode)) or + (eventType == EV_PWR and test_bit(self.EV_PWRevents, eventCode)) or + (eventType == EV_FF_STATUS and + test_bit(self.EV_FF_STATUSevents, eventCode))) + + + def __str__(self): + return ( + "%s\n" + "Bus: %s Vendor: %s Product: %s Version: %s\n" + "Phys: %s\n" + "Sysfs: %s\n" + "Uniq: %s\n" + "Handlers: %s Event Index: %s\n" + "Keyboard: %s Mouse: %s Joystick: %s\n" + "Events: %s" % ( + self.name, self.bus. self.vendor, self.product, self.version, self.phys, + self.sysfs, self.uniq, self.handlers, self.eventIndex, self.isKeyboard, + self.isMouse, self.isJoystick, EvToStr(self.eventTypes))) + + +deviceCapabilities = [] + + +def get_devices(filename = "/proc/bus/input/devices"): + global deviceCapabilities + + with open("/proc/bus/input/devices", "r") as filehandle: + for line in filehandle: + if line[0] == "I": + deviceCapabilities.append(DeviceCapabilities(line, filehandle)) + + return deviceCapabilities + + +def print_devices(): + devs = get_devices() + + for dev in devs: + print(str(dev)) + print(" ABS: {}" + .format([x for x in range(64) if test_bit(dev.EV_ABSevents, x)])) + print(" REL: {}" + .format([x for x in range(64) if test_bit(dev.EV_RELevents, x)])) + print(" MSC: {}" + .format([x for x in range(64) if test_bit(dev.EV_MSCevents, x)])) + print(" KEY: {}" + .format([x for x in range(512) if test_bit(dev.EV_KEYevents, x)])) + print(" LED: {}" + .format([x for x in range(64) if test_bit(dev.EV_LEDevents, x)])) + print() diff --git a/src/py/inevent/Format.py b/src/py/inevent/Format.py new file mode 100644 index 0000000..d91665e --- /dev/null +++ b/src/py/inevent/Format.py @@ -0,0 +1,32 @@ +# The inevent Python module was adapted from pi3d.event from the pi3d +# project. +# +# Copyright (c) 2016, Joseph Coffland, Cauldron Development LLC. +# Copyright (c) 2015, Tim Skillman. +# Copyright (c) 2015, Paddy Gaunt. +# Copyright (c) 2015, Tom Ritchford. +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import struct + +Event = 'llHHi' +EventSize = struct.calcsize(Event) diff --git a/src/py/inevent/InEvent.py b/src/py/inevent/InEvent.py new file mode 100644 index 0000000..fab068a --- /dev/null +++ b/src/py/inevent/InEvent.py @@ -0,0 +1,252 @@ +# The inevent Python module was adapted from pi3d.event from the pi3d +# project. +# +# Copyright (c) 2016, Joseph Coffland, Cauldron Development LLC. +# Copyright (c) 2015, Tim Skillman. +# Copyright (c) 2015, Paddy Gaunt. +# Copyright (c) 2015, Tom Ritchford. +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import pyudev +import re +import select +import errno + +from inevent.EventHandler import EventHandler +from inevent import Keys +from inevent.Constants import * +from inevent.EventStream import EventStream + + +_KEYS = (k for k in vars(Keys) if not k.startswith('_')) +KEY_CODE = dict((k, getattr(Keys, k)) for k in _KEYS) +CODE_KEY = {} +for v in KEY_CODE: CODE_KEY[KEY_CODE[v]] = v + + +def key_to_code(key): + return KEY_CODE.get(str(key), -1) \ + if isinstance(key, str) else key + + +def code_to_key(code): return CODE_KEY.get(code, '') + + +def find_devices(types): + """Finds the event indices of all devices of the specified types. + + A type is a string on the handlers line of /proc/bus/input/devices. + Keyboards use "kbd", mice use "mouse" and joysticks (and gamepads) use "js". + + Returns a list of integer indexes N, where /dev/input/eventN is the event + stream for each device. + + If butNot is given it holds a list of tuples which the returned values should + not match. + + All devices of each type are returned; if you have two mice, they will both + be used. + """ + with open("/proc/bus/input/devices", "r") as filehandle: + for line in filehandle: + if line[0] == "H": + for type in types: + if type in line: + match = re.search("event([0-9]+)", line) + index = match and match.group(1) + if index: yield int(index), type + break + + + +class InEvent(object): + """Encapsulates the entire InEvent subsystem. + + This is generally all you need to import. + + On instantiation, we open all devices that are keyboards, mice or joysticks. + That means we might have two of one sort of another, and that might be a + problem, but it would be rather rare. + + There are several ABS (joystick, touch) events that we do not handle, + specifically THROTTLE, RUDDER, WHEEL, GAS, BRAKE, HAT1, HAT2, HAT3, PRESSURE, + DISTANCE, TILT, TOOL_WIDTH. Implementing these is left as an exercise + for the interested reader. Similarly, we make no attempt to handle + multi-touch. + + Handlers can be supplied, in which case they are called for each event, but + it isn't necessary; API exists for all the events. + + The handler signature is: + + def handler_func(event, state) + + where: + event: + An Event object describing the event. + + state: + An EventState object describing the current state. + + Use key_to_code() to convert from the name of a key to its code, + and code_to_key() to convert a code to a name. + + 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"]): + self.streams = [] + self.handler = EventHandler() + self.types = types + + devs = list(find_devices(types)) + 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() + + + def process_udev_event(self): + action, device = self.udevMon.receive_device() + + match = re.search(r"/dev/input/event([0-9]+)", str(device.device_node)) + devIndex = match and match.group(1) + if not devIndex: return + devIndex = int(devIndex) + + if action == 'add': + for index, devType in find_devices(self.types): + if index == devIndex: + self.add_stream(devIndex, devType) + break + + if action == 'remove': + 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] + + # 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 add_stream(self, devIndex, devType): + try: + self.streams.append(EventStream(devIndex, devType)) + print('Added {}[{:d}]'.format(devType, devIndex)) + + except OSError as e: + if not e.errno in [errno.EPERM, errno.EACCES]: 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 + + + def key_state(self, key): + """ + Returns the state of the given key. + + The returned value will be 0 for key-up, or 1 for key-down. This method + returns a key-held(2) as 1 to aid in using the returned value as a + movement distance. + + This function accepts either the key code or the string name of the key. + It would be more efficient to look-up and store the code of + the key with KEY_CODE[], rather than using the string every time. (Which + involves a dict look-up keyed with a string for every key_state call, every + time around the loop.) + + Gamepad keys are: + Select = BTN_BASE3, Start = BTN_BASE4 + L1 = BTN_TOP R1 = BTN_BASE + L2 = BTN_PINKIE R2 = BTN_BASE2 + + The action buttons are: + BTN_THUMB + BTN_TRIGGER + BTN_TOP + BTN_THUMB2 + + Analogue Left Button = BTN_BASE5 + Analogue Right Button = BTN_BASE6 + + Some of those may clash with extended mouse buttons, so if you are using + both at once, you'll see some overlap. + + The direction pad is hat0 (see get_hat) + """ + return self.handler.key_state(key_to_code(key)) + + + def clear_key(self, key): + """ + Clears the state of the given key. + + Emulates a key-up, but does not call any handlers. + """ + return self.handler.clear_key(key_to_code(key)) + + + def get_keys(self): + return [code_to_key(k) for k in self.handler.get_keys()] + + + def release(self): + """ + Ungrabs all streams and closes all files. + + 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 new file mode 100644 index 0000000..f14b55d --- /dev/null +++ b/src/py/inevent/JogHandler.py @@ -0,0 +1,82 @@ +from inevent.Constants import * + + +def axes_to_string(axes): + return "({:6.3f}, {:6.3f}, {:6.3f}, {:6.3f})".format(*axes) + + +def print_event(event, state): + print("{} {}: ".format(event.get_source(), event.get_type_name()), end = '') + + 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())) + + if event.type == EV_REL: + print("({: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)) + + +class JogHandler: + def __init__(self, config): + self.config = config + self.axes = [0.0, 0.0, 0.0, 0.0] + self.speed = 3 + self.activate = 0 + + + def changed(self): + print(axes_to_string(self.axes) + " x {:d}".format(self.speed)) + + + def __call__(self, event, state): + if event.type not in [EV_ABS, EV_REL, EV_KEY]: return + + changed = False + + # Process event + if event.type == EV_ABS and event.code in self.config['axes']: + pass + + elif event.type == EV_ABS and event.code in self.config['arrows']: + axis = self.config['arrows'].index(event.code) + + if event.value < 0: + if axis == 1: print('up') + else: print('left') + + elif 0 < event.value: + if axis == 1: print('down') + else: print('right') + + elif event.type == EV_KEY and event.code in self.config['speed']: + old_speed = self.speed + self.speed = self.config['speed'].index(event.code) + 1 + if self.speed != old_speed: changed = True + + elif event.type == EV_KEY and event.code in self.config['activate']: + index = self.config['activate'].index(event.code) + + if event.value: self.activate |= 1 << index + else: self.activate &= ~(1 << index) + + if self.config.get('verbose', False): print_event(event, state) + + # Update axes + old_axes = list(self.axes) + + for axis in range(4): + self.axes[axis] = event.stream.state.abs[self.config['axes'][axis]] + if abs(self.axes[axis]) < self.config['deadband']: + self.axes[axis] = 0 + if not (1 << axis) & self.activate and self.activate: + self.axes[axis] = 0 + + if old_axes != self.axes: changed = True + + if changed: self.changed() diff --git a/src/py/inevent/Keys.py b/src/py/inevent/Keys.py new file mode 100644 index 0000000..52b8543 --- /dev/null +++ b/src/py/inevent/Keys.py @@ -0,0 +1,418 @@ +# The inevent Python module was adapted from pi3d.event from the pi3d +# project. +# +# Copyright (c) 2016, Joseph Coffland, Cauldron Development LLC. +# Copyright (c) 2015, Tim Skillman. +# Copyright (c) 2015, Paddy Gaunt. +# Copyright (c) 2015, Tom Ritchford. +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +KEY_ESC = 1 +KEY_1 = 2 +KEY_2 = 3 +KEY_3 = 4 +KEY_4 = 5 +KEY_5 = 6 +KEY_6 = 7 +KEY_7 = 8 +KEY_8 = 9 +KEY_9 = 10 +KEY_0 = 11 +KEY_MINUS = 12 +KEY_EQUAL = 13 +KEY_BACKSPACE = 14 +KEY_TAB = 15 +KEY_Q = 16 +KEY_W = 17 +KEY_E = 18 +KEY_R = 19 +KEY_T = 20 +KEY_Y = 21 +KEY_U = 22 +KEY_I = 23 +KEY_O = 24 +KEY_P = 25 +KEY_LEFTBRACE = 26 +KEY_RIGHTBRACE = 27 +KEY_ENTER = 28 +KEY_LEFTCTRL = 29 +KEY_A = 30 +KEY_S = 31 +KEY_D = 32 +KEY_F = 33 +KEY_G = 34 +KEY_H = 35 +KEY_J = 36 +KEY_K = 37 +KEY_L = 38 +KEY_SEMICOLON = 39 +KEY_APOSTROPHE = 40 +KEY_GRAVE = 41 +KEY_LEFTSHIFT = 42 +KEY_BACKSLASH = 43 +KEY_Z = 44 +KEY_X = 45 +KEY_C = 46 +KEY_V = 47 +KEY_B = 48 +KEY_N = 49 +KEY_M = 50 +KEY_COMMA = 51 +KEY_DOT = 52 +KEY_SLASH = 53 +KEY_RIGHTSHIFT = 54 +KEY_KPASTERISK = 55 +KEY_LEFTALT = 56 +KEY_SPACE = 57 +KEY_CAPSLOCK = 58 +KEY_F1 = 59 +KEY_F2 = 60 +KEY_F3 = 61 +KEY_F4 = 62 +KEY_F5 = 63 +KEY_F6 = 64 +KEY_F7 = 65 +KEY_F8 = 66 +KEY_F9 = 67 +KEY_F10 = 68 +KEY_NUMLOCK = 69 +KEY_SCROLLLOCK = 70 +KEY_KP7 = 71 +KEY_KP8 = 72 +KEY_KP9 = 73 +KEY_KPMINUS = 74 +KEY_KP4 = 75 +KEY_KP5 = 76 +KEY_KP6 = 77 +KEY_KPPLUS = 78 +KEY_KP1 = 79 +KEY_KP2 = 80 +KEY_KP3 = 81 +KEY_KP0 = 82 +KEY_KPDOT = 83 + +KEY_ZENKAKUHANKAKU = 85 +KEY_102ND = 86 +KEY_F11 = 87 +KEY_F12 = 88 +KEY_RO = 89 +KEY_KATAKANA = 90 +KEY_HIRAGANA = 91 +KEY_HENKAN = 92 +KEY_KATAKANAHIRAGANA = 93 +KEY_MUHENKAN = 94 +KEY_KPJPCOMMA = 95 +KEY_KPENTER = 96 +KEY_RIGHTCTRL = 97 +KEY_KPSLASH = 98 +KEY_SYSRQ = 99 +KEY_RIGHTALT = 100 +KEY_LINEFEED = 101 +KEY_HOME = 102 +KEY_UP = 103 +KEY_PAGEUP = 104 +KEY_LEFT = 105 +KEY_RIGHT = 106 +KEY_END = 107 +KEY_DOWN = 108 +KEY_PAGEDOWN = 109 +KEY_INSERT = 110 +KEY_DELETE = 111 +KEY_MACRO = 112 +KEY_MUTE = 113 +KEY_VOLUMEDOWN = 114 +KEY_VOLUMEUP = 115 +KEY_POWER = 116 +KEY_KPEQUAL = 117 +KEY_KPPLUSMINUS = 118 +KEY_PAUSE = 119 + +KEY_KPCOMMA = 121 +KEY_HANGUEL = 122 +KEY_HANJA = 123 +KEY_YEN = 124 +KEY_LEFTMETA = 125 +KEY_RIGHTMETA = 126 +KEY_COMPOSE = 127 + +KEY_STOP = 128 +KEY_AGAIN = 129 +KEY_PROPS = 130 +KEY_UNDO = 131 +KEY_FRONT = 132 +KEY_COPY = 133 +KEY_OPEN = 134 +KEY_PASTE = 135 +KEY_FIND = 136 +KEY_CUT = 137 +KEY_HELP = 138 +KEY_MENU = 139 +KEY_CALC = 140 +KEY_SETUP = 141 +KEY_SLEEP = 142 +KEY_WAKEUP = 143 +KEY_FILE = 144 +KEY_SENDFILE = 145 +KEY_DELETEFILE = 146 +KEY_XFER = 147 +KEY_PROG1 = 148 +KEY_PROG2 = 149 +KEY_WWW = 150 +KEY_MSDOS = 151 +KEY_COFFEE = 152 +KEY_DIRECTION = 153 +KEY_CYCLEWINDOWS = 154 +KEY_MAIL = 155 +KEY_BOOKMARKS = 156 +KEY_COMPUTER = 157 +KEY_BACK = 158 +KEY_FORWARD = 159 +KEY_CLOSECD = 160 +KEY_EJECTCD = 161 +KEY_EJECTCLOSECD = 162 +KEY_NEXTSONG = 163 +KEY_PLAYPAUSE = 164 +KEY_PREVIOUSSONG = 165 +KEY_STOPCD = 166 +KEY_RECORD = 167 +KEY_REWIND = 168 +KEY_PHONE = 169 +KEY_ISO = 170 +KEY_CONFIG = 171 +KEY_HOMEPAGE = 172 +KEY_REFRESH = 173 +KEY_EXIT = 174 +KEY_MOVE = 175 +KEY_EDIT = 176 +KEY_SCROLLUP = 177 +KEY_SCROLLDOWN = 178 +KEY_KPLEFTPAREN = 179 +KEY_KPRIGHTPAREN = 180 + +KEY_F13 = 183 +KEY_F14 = 184 +KEY_F15 = 185 +KEY_F16 = 186 +KEY_F17 = 187 +KEY_F18 = 188 +KEY_F19 = 189 +KEY_F20 = 190 +KEY_F21 = 191 +KEY_F22 = 192 +KEY_F23 = 193 +KEY_F24 = 194 + +KEY_PLAYCD = 200 +KEY_PAUSECD = 201 +KEY_PROG3 = 202 +KEY_PROG4 = 203 +KEY_SUSPEND = 205 +KEY_CLOSE = 206 +KEY_PLAY = 207 +KEY_FASTFORWARD = 208 +KEY_BASSBOOST = 209 +KEY_PRINT = 210 +KEY_HP = 211 +KEY_CAMERA = 212 +KEY_SOUND = 213 +KEY_QUESTION = 214 +KEY_EMAIL = 215 +KEY_CHAT = 216 +KEY_SEARCH = 217 +KEY_CONNECT = 218 +KEY_FINANCE = 219 +KEY_SPORT = 220 +KEY_SHOP = 221 +KEY_ALTERASE = 222 +KEY_CANCEL = 223 +KEY_BRIGHTNESSDOWN = 224 +KEY_BRIGHTNESSUP = 225 +KEY_MEDIA = 226 + +KEY_UNKNOWN = 240 + +BTN_MISC = 0x100 +BTN_0 = 0x100 +BTN_1 = 0x101 +BTN_2 = 0x102 +BTN_3 = 0x103 +BTN_4 = 0x104 +BTN_5 = 0x105 +BTN_6 = 0x106 +BTN_7 = 0x107 +BTN_8 = 0x108 +BTN_9 = 0x109 + +BTN_MOUSE = 0x110 +BTN_LEFT = 0x110 +BTN_RIGHT = 0x111 +BTN_MIDDLE = 0x112 +BTN_SIDE = 0x113 +BTN_EXTRA = 0x114 +BTN_FORWARD = 0x115 +BTN_BACK = 0x116 +BTN_TASK = 0x117 + +BTN_JOYSTICK = 0x120 +BTN_TRIGGER = 0x120 +BTN_THUMB = 0x121 +BTN_THUMB2 = 0x122 +BTN_TOP = 0x123 +BTN_TOP2 = 0x124 +BTN_PINKIE = 0x125 +BTN_BASE = 0x126 +BTN_BASE2 = 0x127 +BTN_BASE3 = 0x128 +BTN_BASE4 = 0x129 +BTN_BASE5 = 0x12a +BTN_BASE6 = 0x12b +BTN_DEAD = 0x12f + +BTN_GAMEPAD = 0x130 +BTN_A = 0x130 +BTN_B = 0x131 +BTN_C = 0x132 +BTN_X = 0x133 +BTN_Y = 0x134 +BTN_Z = 0x135 +BTN_TL = 0x136 +BTN_TR = 0x137 +BTN_TL2 = 0x138 +BTN_TR2 = 0x139 +BTN_SELECT = 0x13a +BTN_START = 0x13b +BTN_MODE = 0x13c +BTN_THUMBL = 0x13d +BTN_THUMBR = 0x13e + +BTN_DIGI = 0x140 +BTN_TOOL_PEN = 0x140 +BTN_TOOL_RUBBER = 0x141 +BTN_TOOL_BRUSH = 0x142 +BTN_TOOL_PENCIL = 0x143 +BTN_TOOL_AIRBRUSH = 0x144 +BTN_TOOL_FINGER = 0x145 +BTN_TOOL_MOUSE = 0x146 +BTN_TOOL_LENS = 0x147 +BTN_TOUCH = 0x14a +BTN_STYLUS = 0x14b +BTN_STYLUS2 = 0x14c +BTN_TOOL_DOUBLETAP = 0x14d +BTN_TOOL_TRIPLETAP = 0x14e + +BTN_WHEEL = 0x150 +BTN_GEAR_DOWN = 0x150 +BTN_GEAR_UP = 0x151 + +KEY_OK = 0x160 +KEY_SELECT = 0x161 +KEY_GOTO = 0x162 +KEY_CLEAR = 0x163 +KEY_POWER2 = 0x164 +KEY_OPTION = 0x165 +KEY_INFO = 0x166 +KEY_TIME = 0x167 +KEY_VENDOR = 0x168 +KEY_ARCHIVE = 0x169 +KEY_PROGRAM = 0x16a +KEY_CHANNEL = 0x16b +KEY_FAVORITES = 0x16c +KEY_EPG = 0x16d +KEY_PVR = 0x16e +KEY_MHP = 0x16f +KEY_LANGUAGE = 0x170 +KEY_TITLE = 0x171 +KEY_SUBTITLE = 0x172 +KEY_ANGLE = 0x173 +KEY_ZOOM = 0x174 +KEY_MODE = 0x175 +KEY_KEYBOARD = 0x176 +KEY_SCREEN = 0x177 +KEY_PC = 0x178 +KEY_TV = 0x179 +KEY_TV2 = 0x17a +KEY_VCR = 0x17b +KEY_VCR2 = 0x17c +KEY_SAT = 0x17d +KEY_SAT2 = 0x17e +KEY_CD = 0x17f +KEY_TAPE = 0x180 +KEY_RADIO = 0x181 +KEY_TUNER = 0x182 +KEY_PLAYER = 0x183 +KEY_TEXT = 0x184 +KEY_DVD = 0x185 +KEY_AUX = 0x186 +KEY_MP3 = 0x187 +KEY_AUDIO = 0x188 +KEY_VIDEO = 0x189 +KEY_DIRECTORY = 0x18a +KEY_LIST = 0x18b +KEY_MEMO = 0x18c +KEY_CALENDAR = 0x18d +KEY_RED = 0x18e +KEY_GREEN = 0x18f +KEY_YELLOW = 0x190 +KEY_BLUE = 0x191 +KEY_CHANNELUP = 0x192 +KEY_CHANNELDOWN = 0x193 +KEY_FIRST = 0x194 +KEY_LAST = 0x195 +KEY_AB = 0x196 +KEY_NEXT = 0x197 +KEY_RESTART = 0x198 +KEY_SLOW = 0x199 +KEY_SHUFFLE = 0x19a +KEY_BREAK = 0x19b +KEY_PREVIOUS = 0x19c +KEY_DIGITS = 0x19d +KEY_TEEN = 0x19e +KEY_TWEN = 0x19f + +KEY_DEL_EOL = 0x1c0 +KEY_DEL_EOS = 0x1c1 +KEY_INS_LINE = 0x1c2 +KEY_DEL_LINE = 0x1c3 + +KEY_FN = 0x1d0 +KEY_FN_ESC = 0x1d1 +KEY_FN_F1 = 0x1d2 +KEY_FN_F2 = 0x1d3 +KEY_FN_F3 = 0x1d4 +KEY_FN_F4 = 0x1d5 +KEY_FN_F5 = 0x1d6 +KEY_FN_F6 = 0x1d7 +KEY_FN_F7 = 0x1d8 +KEY_FN_F8 = 0x1d9 +KEY_FN_F9 = 0x1da +KEY_FN_F10 = 0x1db +KEY_FN_F11 = 0x1dc +KEY_FN_F12 = 0x1dd +KEY_FN_1 = 0x1de +KEY_FN_2 = 0x1df +KEY_FN_D = 0x1e0 +KEY_FN_E = 0x1e1 +KEY_FN_F = 0x1e2 +KEY_FN_S = 0x1e3 +KEY_FN_B = 0x1e4 + +KEY_MAX = 0x1ff diff --git a/src/py/inevent/__init__.py b/src/py/inevent/__init__.py new file mode 100644 index 0000000..6ab1928 --- /dev/null +++ b/src/py/inevent/__init__.py @@ -0,0 +1,30 @@ +# The inevent Python module was adapted from pi3d.event from the pi3d +# project. +# +# Copyright (c) 2016, Joseph Coffland, Cauldron Development LLC. +# Copyright (c) 2015, Tim Skillman. +# Copyright (c) 2015, Paddy Gaunt. +# Copyright (c) 2015, Tom Ritchford. +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from .InEvent import InEvent +from .JogHandler import JogHandler diff --git a/src/py/inevent/ioctl.py b/src/py/inevent/ioctl.py new file mode 100644 index 0000000..6e91b40 --- /dev/null +++ b/src/py/inevent/ioctl.py @@ -0,0 +1,101 @@ +# The inevent Python module was adapted from pi3d.event from the pi3d +# project. +# +# Copyright (c) 2016, Joseph Coffland, Cauldron Development LLC. +# Copyright (c) 2015, Tim Skillman. +# Copyright (c) 2015, Paddy Gaunt. +# Copyright (c) 2015, Tom Ritchford. +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# IOCTL macros +# +# ioctl command encoding: 32 bits total, command in lower 16 bits, +# size of the parameter structure in the lower 14 bits of the +# upper 16 bits. +# +# Encoding the size of the parameter structure in the ioctl request +# is useful for catching programs compiled with old versions +# and to avoid overwriting user space outside the user buffer area. +# The highest 2 bits are reserved for indicating the ``access mode''. +# NOTE: This limits the max parameter size to 16kB - 1 +# +# The following is for compatibility across the various Linux +# platforms. The generic ioctl numbering scheme doesn't really enforce +# a type field. De facto, however, the top 8 bits of the lower 16 +# bits are indeed used as a type field, so we might just as well make +# this explicit here. + +import struct + + +sizeof = struct.calcsize + +_IOC_NRBITS = 8 +_IOC_TYPEBITS = 8 +_IOC_SIZEBITS = 14 +_IOC_DIRBITS = 2 + +_IOC_NRMASK = (1 << _IOC_NRBITS) - 1 +_IOC_TYPEMASK = (1 << _IOC_TYPEBITS) - 1 +_IOC_SIZEMASK = (1 << _IOC_SIZEBITS) - 1 +_IOC_DIRMASK = (1 << _IOC_DIRBITS) - 1 + +_IOC_NRSHIFT = 0 +_IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS +_IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS +_IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS + +_IOC_NONE = 0 +_IOC_WRITE = 1 +_IOC_READ = 2 +_IOC_RW = _IOC_READ | _IOC_WRITE + + +def _IOC(dir, type, nr, size): + return int( + (dir << _IOC_DIRSHIFT) | + (type << _IOC_TYPESHIFT) | + (nr << _IOC_NRSHIFT) | + (size << _IOC_SIZESHIFT)) + +# encode ioctl numbers +def _IO(type, nr): return _IOC(_IOC_NONE, type, nr, 0) +def _IOR(type, nr, fmt): return _IOC(_IOC_READ, type, nr, sizeof(fmt)) +def _IOW(type, nr, fmt): return _IOC(_IOC_WRITE, type, nr, sizeof(fmt)) +def _IOWR(type, nr, fmt): return _IOC(_IOC_RW, type, nr, sizeof(fmt)) +def _IOR_BAD(type, nr, fmt): return _IOC(_IOC_READ, type, nr, sizeof(fmt)) +def _IOW_BAD(type, nr, fmt): return _IOC(_IOC_WRITE, type, nr, sizeof(fmt)) +def _IOWR_BAD(type, nr, fmt): return _IOC(_IOC_RW, type, nr, sizeof(fmt)) + +# decode ioctl numbers +def _IOC_DIR(nr): return (nr >> _IOC_DIRSHIFT) & _IOC_DIRMASK +def _IOC_TYPE(nr): return (nr >> _IOC_TYPESHIFT) & _IOC_TYPEMASK +def _IOC_NR(nr): return (nr >> _IOC_NRSHIFT) & _IOC_NRMASK +def _IOC_SIZE(nr): return (nr >> _IOC_SIZESHIFT) & _IOC_SIZEMASK + +# for drivers/sound files +IOC_IN = _IOC_WRITE << _IOC_DIRSHIFT +IOC_OUT = _IOC_READ << _IOC_DIRSHIFT +IOC_INOUT = _IOC_RW << _IOC_DIRSHIFT +IOCSIZE_MASK = _IOC_SIZEMASK << _IOC_SIZESHIFT +IOCSIZE_SHIFT = _IOC_SIZESHIFT + diff --git a/src/py/lcd/__init__.py b/src/py/lcd/__init__.py new file mode 100755 index 0000000..0380705 --- /dev/null +++ b/src/py/lcd/__init__.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 + +try: + import smbus +except: + import smbus2 as smbus + +import time + + +# Control flags +REG_SELECT_BIT = 1 << 0 +READ_BIT = 1 << 1 +ENABLE_BIT = 1 << 2 +BACKLIGHT_BIT = 1 << 3 + +# Commands +LCD_CLEAR_DISPLAY = 1 << 0 +LCD_RETURN_HOME = 1 << 1 +LCD_ENTRY_MODE_SET = 1 << 2 +LCD_DISPLAY_CONTROL = 1 << 3 +LCD_CURSOR_SHIFT = 1 << 4 +LCD_FUNCTION_SET = 1 << 5 +LCD_SET_CGRAM_ADDR = 1 << 6 +LCD_SET_DDRAM_ADDR = 1 << 7 + +# Entry Mode Set flags +LCD_ENTRY_SHIFT_DISPLAY = 1 << 0 +LCD_ENTRY_SHIFT_INC = 1 << 1 +LCD_ENTRY_SHIFT_DEC = 0 << 1 + +# Display Control flags +LCD_BLINK_ON = 1 << 0 +LCD_BLINK_OFF = 0 << 0 +LCD_CURSOR_ON = 1 << 1 +LCD_CURSOR_OFF = 0 << 1 +LCD_DISPLAY_ON = 1 << 2 +LCD_DISPLAY_OFF = 0 << 2 + +# Cursor Shift flags +LCD_SHIFT_RIGHT = 1 << 2 +LCD_SHIFT_LEFT = 0 << 2 +LCD_SHIFT_DISPLAY = 1 << 3 +LCD_SHIFT_CURSOR = 0 << 3 + +# Function Set flags +LCD_5x11_DOTS = 1 << 2 +LCD_5x8_DOTS = 0 << 2 +LCD_2_LINE = 1 << 3 +LCD_1_LINE = 0 << 3 +LCD_8_BIT_MODE = 1 << 4 +LCD_4_BIT_MODE = 0 << 4 + +# Text justification flags +JUSTIFY_LEFT = 0 +JUSTIFY_RIGHT = 1 +JUSTIFY_CENTER = 2 + + + +class LCD: + def __init__(self, port, addr, height = 4, width = 20): + self.addr = addr + self.height = height + self.width = width + + try: + self.bus = smbus.SMBus(port) + except FileNotFoundError as e: + self.bus = None + print('Failed to open LCD device:', e) + + self.backlight = True + + self.reset() + + + def reset(self): + self.clear() + self.write(LCD_FUNCTION_SET | LCD_2_LINE | LCD_5x8_DOTS | + LCD_4_BIT_MODE) + self.write(LCD_DISPLAY_CONTROL | LCD_DISPLAY_ON) + self.write(LCD_ENTRY_MODE_SET | LCD_ENTRY_SHIFT_INC) + + + def write_i2c(self, data): + if self.bus is None: return + + if self.backlight: data |= BACKLIGHT_BIT + + self.bus.write_byte(self.addr, data) + time.sleep(0.0001) + + + # Write half of a command to LCD + def write_nibble(self, data): + self.write_i2c(data) + + # Strobe + self.write_i2c(data | ENABLE_BIT) + time.sleep(0.0005) + + self.write_i2c(data & ~ENABLE_BIT) + time.sleep(0.0001) + + + # Write an 8-bit command to LCD + def write(self, cmd, flags = 0): + self.write_nibble(flags | (cmd & 0xf0)) + self.write_nibble(flags | ((cmd << 4) & 0xf0)) + + + def set_cursor(self, on, blink): + data = LCD_DISPLAY_CONTROL + + if on: data |= LCD_CURSOR_ON + if blink: data |= LCD_BLINK_ON + + self.write(data) + + + def set_backlight(self, enable): + self.backlight = enable + self.write_i2c(0) + + + def program_char(self, addr, data): + if addr < 0 or 8 <= addr: return + + self.write(LCD_SET_CGRAM_ADDR | (addr << 3)) + for x in data: + self.write(x, REG_SELECT_BIT) + + + def goto(self, x, y): + if x < 0 or self.width <= x or y < 0 or self.height <= y: return + self.write(LCD_SET_DDRAM_ADDR | (0, 64, 20, 84)[y] + int(x)) + + + def text(self, msg): + for c in msg: + self.write(ord(c), REG_SELECT_BIT) + + + def display(self, line, msg, justify = JUSTIFY_LEFT): + if justify == JUSTIFY_RIGHT: x = self.width - len(msg) + elif justify == JUSTIFY_CENTER: x = (self.width - len(msg)) / 2 + else: x = 0 + + if x < 0: x = 0 + + self.goto(x, line) + self.text(msg) + + + def shift(self, count = 1, right = True, display = True): + cmd = LCD_CURSOR_SHIFT + if right: cmd |= LCD_SHIFT_RIGHT + if display: cmd |= LCD_SHIFT_DISPLAY + + for i in range(count): self.write(cmd) + + + # Clear LCD and move cursor home + def clear(self): + self.write(LCD_CLEAR_DISPLAY) + self.write(LCD_RETURN_HOME) + + + +if __name__ == "__main__": + lcd = LCD(1, 0x27) + + lcd.clear() + + lcd.program_char(0, (0b11011, + 0b11011, + 0b00000, + 0b01100, + 0b01100, + 0b00000, + 0b11011, + 0b11011)) + + lcd.program_char(1, (0b11000, + 0b01100, + 0b00110, + 0b00011, + 0b00011, + 0b00110, + 0b01100, + 0b11000)) + + lcd.program_char(2, (0b00011, + 0b00110, + 0b01100, + 0b11000, + 0b11000, + 0b01100, + 0b00110, + 0b00011)) + + lcd.display(0, '\0' * lcd.width) + lcd.display(1, 'Hello world!', JUSTIFY_CENTER) + lcd.display(2, '\1\2' * (lcd.width / 2)) + lcd.display(3, '12345678901234567890') diff --git a/src/py/lcd/splash.py b/src/py/lcd/splash.py new file mode 100755 index 0000000..d4277c1 --- /dev/null +++ b/src/py/lcd/splash.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 + +import lcd + +if __name__ == "__main__": + screen = lcd.LCD(1, 0x27) + + screen.clear() + screen.display(0, 'Buildbotics', lcd.JUSTIFY_CENTER) + screen.display(1, 'Controller', lcd.JUSTIFY_CENTER) + screen.display(3, 'Booting...', lcd.JUSTIFY_CENTER)