#!/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, logging
import sys
import struct
import exceptions
import threading

logging.basicConfig(level=logging.INFO)
log = logging.getLogger()

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__':
    def ev(what):
        print 'ev', what

    p = powermate("/dev/input/by-id/usb-Contour_Design_ShuttlePRO-event-if00", ev)
    while True:
        p.read_next()
