changeset 43:6fbb05317eef

adapt gnuradio shuttlepro code Ignore-this: c57deda5bb395bd221bf27b9705dbf6a
author drewp@bigasterisk.com
date Sun, 02 Dec 2012 16:58:45 -0800
parents 022921fdafd0
children 55b68d1e8212
files service/shuttlepro/shuttlepro.py
diffstat 1 files changed, 364 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/shuttlepro/shuttlepro.py	Sun Dec 02 16:58:45 2012 -0800
@@ -0,0 +1,364 @@
+#!/usr/bin/env python
+#
+# Copyright 2005 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3, or (at your option)
+# any later version.
+#
+# GNU Radio is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GNU Radio; see the file COPYING.  If not, write to
+# the Free Software Foundation, Inc., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+"""
+Handler for Griffin PowerMate, Contour ShuttlePro & ShuttleXpress USB knobs
+
+modified by drewp@bigasterisk.com
+"""
+
+import os, time
+import sys
+import struct
+import exceptions
+import threading
+
+
+def hexint(mask):
+  """
+  Convert unsigned masks into signed ints.
+
+  This allows us to use hex constants like 0xf0f0f0f2 when talking to
+  our hardware and not get screwed by them getting treated as python
+  longs.
+  """
+  if mask >= 2**31:
+     return int(mask-2**32)
+  return mask
+imported_ok = True
+
+try:
+    import select
+    import fcntl
+except ImportError:
+    imported_ok = False
+
+
+# First a little bit of background:
+#
+# The Griffin PowerMate has
+#  * a single knob which rotates
+#  * a single button (pressing the knob)
+#
+# The Contour ShuttleXpress (aka SpaceShuttle) has
+#  * "Jog Wheel"  -- the knob (rotary encoder) on the inside
+#  * "Shuttle Ring" -- the spring loaded rubber covered ring
+#  * 5 buttons
+#
+# The Contour ShuttlePro has
+#  * "Jog Wheel" -- the knob (rotary encoder) on the inside
+#  * "Shuttle Ring" -- the spring loaded rubber covered ring
+#  * 13 buttons
+#
+# The Contour ShuttlePro V2 has
+#  *"Jog Wheel" -- the knob (rotary encoder) on the inside
+#  * "Shuttle Ring" -- the spring loaded rubber covered ring
+#  * 15 buttons
+
+# We remap all the buttons on the devices so that they start at zero.
+
+# For the ShuttleXpress the buttons are 0 to 4 (left to right)
+
+# For the ShuttlePro, we number the buttons immediately above
+# the ring 0 to 4 (left to right) so that they match our numbering
+# on the ShuttleXpress.  The top row is 5, 6, 7, 8.  The first row below
+# the ring is 9, 10, and the bottom row is 11, 12.
+
+# For the ShuttlePro V2, buttons 13 & 14 are to the
+# left and right of the wheel respectively.
+
+# We generate 3 kinds of events:
+#
+#   button press/release (button_number, press/release)
+#   knob rotation (relative_clicks)       # typically -1, +1
+#   shuttle position (absolute_position)  # -7,-6,...,0,...,6,7
+
+# ----------------------------------------------------------------
+# Our ID's for the devices:
+# Not to be confused with anything related to magic hardware numbers.
+
+ID_POWERMATE         = 'powermate'
+ID_SHUTTLE_XPRESS    = 'shuttle xpress'
+ID_SHUTTLE_PRO       = 'shuttle pro'
+ID_SHUTTLE_PRO_V2    = 'shuttle pro v2'
+
+# ------------------------------------------------------------------------
+# format of messages that we read from /dev/input/event*
+# See /usr/include/linux/input.h for more info
+#
+#struct input_event {
+#        struct timeval time; = {long seconds, long microseconds}
+#        unsigned short type;
+#        unsigned short code;
+#        unsigned int value;
+#};
+
+input_event_struct = "@llHHi"
+input_event_size = struct.calcsize(input_event_struct)
+
+# ------------------------------------------------------------------------
+# input_event types
+# ------------------------------------------------------------------------
+
+IET_SYN		  = 0x00   # aka RESET
+IET_KEY		  = 0x01   # key or button press/release
+IET_REL		  = 0x02   # relative movement (knob rotation)
+IET_ABS		  = 0x03   # absolute position (graphics pad, etc)
+IET_MSC		  = 0x04
+IET_LED		  = 0x11
+IET_SND		  = 0x12
+IET_REP		  = 0x14
+IET_FF		  = 0x15
+IET_PWR		  = 0x16
+IET_FF_STATUS	  = 0x17
+IET_MAX		  = 0x1f
+
+# ------------------------------------------------------------------------
+# input_event codes (there are a zillion of them, we only define a few)
+# ------------------------------------------------------------------------
+
+# these are valid for IET_KEY
+
+IEC_BTN_0	   = 0x100
+IEC_BTN_1	   = 0x101
+IEC_BTN_2	   = 0x102
+IEC_BTN_3	   = 0x103
+IEC_BTN_4	   = 0x104
+IEC_BTN_5	   = 0x105
+IEC_BTN_6	   = 0x106
+IEC_BTN_7	   = 0x107
+IEC_BTN_8	   = 0x108
+IEC_BTN_9	   = 0x109
+IEC_BTN_10	   = 0x10a
+IEC_BTN_11	   = 0x10b
+IEC_BTN_12	   = 0x10c
+IEC_BTN_13	   = 0x10d
+IEC_BTN_14	   = 0x10e
+IEC_BTN_15	   = 0x10f
+
+# these are valid for IET_REL (Relative axes)
+
+IEC_REL_X	   = 0x00
+IEC_REL_Y	   = 0x01
+IEC_REL_Z	   = 0x02
+IEC_REL_HWHEEL	   = 0x06
+IEC_REL_DIAL	   = 0x07   # rotating the knob
+IEC_REL_WHEEL	   = 0x08   # moving the shuttle ring
+IEC_REL_MISC	   = 0x09
+IEC_REL_MAX	   = 0x0f
+
+# ------------------------------------------------------------------------
+
+class powermate(object):
+    """
+    Interface to Griffin PowerMate and Contour Shuttles
+    """
+    def __init__(self, filename=None, on_event=lambda ev: None):
+        self.on_event = on_event
+        self.handle = -1
+        if not imported_ok:
+            raise exceptions.RuntimeError, 'powermate not supported on this platform'
+
+        if filename:
+            if not self._open_device(filename):
+                raise exceptions.RuntimeError, 'Unable to find powermate'
+        else:
+            ok = False
+            for d in range(0, 16):
+                if self._open_device("/dev/input/event%d" % d):
+                    ok = True
+                    break
+            if not ok:
+                raise exceptions.RuntimeError, 'Unable to find powermate'
+
+    def __del__(self):
+        self.keep_running = False
+        if self.handle >= 0:
+            os.close(self.handle)
+            self.handle = -1
+
+    def _open_device(self, filename):
+        try:
+            self.handle = os.open(filename, os.O_RDWR)
+            if self.handle < 0:
+                print "can't open file"
+                return False
+
+            # read event device name
+            name = fcntl.ioctl(self.handle, hexint(0x80ff4506), chr(0) * 256)
+            name = name.replace(chr(0), '')
+            print "%s name is %s" % (filename, name)
+            # do we see anything we recognize?
+            if name == 'Griffin PowerMate' or name == 'Griffin SoundKnob':
+                self.id = ID_POWERMATE
+                self.mapper = _powermate_remapper()
+            elif name == 'CAVS SpaceShuttle A/V' or name == 'Contour Design ShuttleXpress':
+                self.id = ID_SHUTTLE_XPRESS
+                self.mapper = _contour_remapper()
+            elif name == 'Contour Design ShuttlePRO':
+                self.id = ID_SHUTTLE_PRO
+                self.mapper = _contour_remapper()
+            elif name == 'Contour Design ShuttlePRO v2':
+                self.id = ID_SHUTTLE_PRO_V2
+                self.mapper = _contour_remapper()
+            else:
+                os.close(self.handle)
+                self.handle = -1
+                return False
+
+            # get exclusive control of the device, using ioctl EVIOCGRAB
+	    # there may be an issue with this on non x86 platforms and if
+	    # the _IOW,_IOC,... macros in <asm/ioctl.h> are changed
+            fcntl.ioctl(self.handle,hexint(0x40044590), 1)
+            return True
+        except exceptions.OSError:
+            return False
+
+
+    def set_event_receiver(self, obj):
+        self.event_receiver = obj
+
+
+    def set_led_state(self, static_brightness, pulse_speed=0,
+                      pulse_table=0, pulse_on_sleep=0, pulse_on_wake=0):
+        """
+        What do these magic values mean...
+        """
+        if self.id != ID_POWERMATE:
+            return False
+
+        static_brightness &= 0xff;
+        if pulse_speed < 0:
+            pulse_speed = 0
+        if pulse_speed > 510:
+            pulse_speed = 510
+        if pulse_table < 0:
+            pulse_table = 0
+        if pulse_table > 2:
+            pulse_table = 2
+        pulse_on_sleep = not not pulse_on_sleep # not not = convert to 0/1
+        pulse_on_wake  = not not pulse_on_wake
+        magic = (static_brightness
+                 | (pulse_speed << 8)
+                 | (pulse_table << 17)
+                 | (pulse_on_sleep << 19)
+                 | (pulse_on_wake << 20))
+        data = struct.pack(input_event_struct, 0, 0, 0x04, 0x01, magic)
+        os.write(self.handle, data)
+        return True
+
+    def read_next(self):
+        s = os.read (self.handle, input_event_size)
+        if not s:
+            return
+
+        raw_input_event = struct.unpack(input_event_struct,s)
+        sec, usec, type, code, val = self.mapper(raw_input_event)
+
+        if type == IET_SYN:    # ignore
+            pass
+        elif type == IET_MSC:  # ignore (seems to be PowerMate reporting led brightness)
+            pass
+        elif type == IET_REL and code == IEC_REL_DIAL:
+            self.on_event({"dial":val})
+        elif type == IET_REL and code == IEC_REL_WHEEL:
+            self.on_event({"shuttle":val})
+        elif type == IET_KEY:
+            self.on_event({"key":{"button":code - IEC_BTN_0, "press":val}})
+        else:
+            print "powermate: unrecognized event: type = 0x%x  code = 0x%x  val = %d" % (type, code, val)
+
+
+class _powermate_remapper(object):
+    def __init__(self):
+        pass
+    def __call__(self, event):
+        """
+        Notice how nice and simple this is...
+        """
+        return event
+
+class _contour_remapper(object):
+    def __init__(self):
+        self.prev = None
+    def __call__(self, event):
+        """
+        ...and how screwed up this is
+        """
+        sec, usec, type, code, val = event
+        if type == IET_REL and code == IEC_REL_WHEEL:
+            # === Shuttle ring ===
+            # First off, this really ought to be IET_ABS, not IET_REL!
+            # They never generate a zero value so you can't
+            # tell when the shuttle ring is back in the center.
+            # We kludge around this by calling both -1 and 1 zero.
+            if val == -1 or val == 1:
+                return (sec, usec, type, code, 0)
+            return event
+
+        if type == IET_REL and code == IEC_REL_DIAL:
+            # === Jog knob (rotary encoder) ===
+            # Dim wits got it wrong again!  This one should return a
+            # a relative value, e.g., -1, +1.  Instead they return
+            # a total that runs modulo 256 (almost!).   For some
+            # reason they count like this 253, 254, 255, 1, 2, 3
+
+            if self.prev is None:                  # first time call
+                self.prev = val
+                return (sec, usec, IET_SYN, 0, 0)  # will be ignored above
+
+            diff = val - self.prev
+            if diff == 0:                          # sometimes it just sends stuff...
+                return (sec, usec, IET_SYN, 0, 0)  # will be ignored above
+
+            if abs(diff) > 100:      # crossed into the twilight zone
+                if self.prev > val:  # we've wrapped going forward
+                    self.prev = val
+                    return (sec, usec, type, code, +1)
+                else:                # we've wrapped going backward
+                    self.prev = val
+                    return (sec, usec, type, code, -1)
+
+            self.prev = val
+            return (sec, usec, type, code, diff)
+
+        if type == IET_KEY:
+            # remap keys so that all 3 gadgets have buttons 0 to 4 in common
+            return (sec, usec, type,
+                    (IEC_BTN_5, IEC_BTN_6, IEC_BTN_7, IEC_BTN_8,
+                     IEC_BTN_0, IEC_BTN_1, IEC_BTN_2, IEC_BTN_3, IEC_BTN_4,
+                     IEC_BTN_9,  IEC_BTN_10,
+                     IEC_BTN_11, IEC_BTN_12,
+                     IEC_BTN_13, IEC_BTN_14)[code - IEC_BTN_0], val)
+
+        return event
+
+if __name__ == '__main__':
+    import restkit
+    mpd = restkit.Resource("http://bang:9009/")
+    def ev(what):
+        print 'ev', what
+        if 'key' in what and what['press']:
+            print mpd.post("addAndPlay/%s" % what['key']['button']).body_string()
+    p = powermate("/dev/input/by-id/usb-Contour_Design_ShuttlePRO-event-if00", ev)
+    while True:
+        p.read_next()