summaryrefslogtreecommitdiff
path: root/gr-wxgui/src/python/powermate.py
blob: 041a2cd6b72fe27f28f5c4da011e219765737d48 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
#!/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 2, 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

This is Linux and wxPython specific.
"""

import os
import sys
import struct
import exceptions
import threading
import wx
from gnuradio import gru

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(threading.Thread):
    """
    Interface to Griffin PowerMate and Contour Shuttles
    """
    def __init__(self, event_receiver=None, filename=None, **kwargs):
        self.event_receiver = event_receiver
        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'

        threading.Thread.__init__(self, **kwargs)
        self.setDaemon (1)
        self.keep_running = True
        self.start ()
        
    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:
                return False

            # read event device name
            name = fcntl.ioctl(self.handle, gru.hexint(0x80ff4506), chr(0) * 256)
            name = name.replace(chr(0), '')

            # 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,gru.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 run (self):
        while (self.keep_running):
            s = os.read (self.handle, input_event_size)
            if not s:
                self.keep_running = False
                break

            raw_input_event = struct.unpack(input_event_struct,s)
            sec, usec, type, code, val = self.mapper(raw_input_event)
            
            if self.event_receiver is None:
                continue
            
            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:
                #print "Dial: %d" % (val,)
                wx.PostEvent(self.event_receiver, PMRotateEvent(val))
            elif type == IET_REL and code == IEC_REL_WHEEL:
                #print "Shuttle: %d" % (val,)
                wx.PostEvent(self.event_receiver, PMShuttleEvent(val))
            elif type == IET_KEY:
                #print "Key: Btn%d %d" % (code - IEC_BTN_0, val)
                wx.PostEvent(self.event_receiver,
                             PMButtonEvent(code - IEC_BTN_0, 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

# ------------------------------------------------------------------------
# new wxPython event classes
# ------------------------------------------------------------------------

grEVT_POWERMATE_BUTTON  = wx.NewEventType()
grEVT_POWERMATE_ROTATE  = wx.NewEventType()
grEVT_POWERMATE_SHUTTLE = wx.NewEventType()

EVT_POWERMATE_BUTTON = wx.PyEventBinder(grEVT_POWERMATE_BUTTON, 0)
EVT_POWERMATE_ROTATE = wx.PyEventBinder(grEVT_POWERMATE_ROTATE, 0)
EVT_POWERMATE_SHUTTLE = wx.PyEventBinder(grEVT_POWERMATE_SHUTTLE, 0)

class PMButtonEvent(wx.PyEvent):
    def __init__(self, button, value):
        wx.PyEvent.__init__(self)
        self.SetEventType(grEVT_POWERMATE_BUTTON)
        self.button = button
        self.value = value

    def Clone (self): 
        self.__class__(self.GetId())
        

class PMRotateEvent(wx.PyEvent):
    def __init__(self, delta):
        wx.PyEvent.__init__(self)
        self.SetEventType (grEVT_POWERMATE_ROTATE)
        self.delta = delta

    def Clone (self): 
        self.__class__(self.GetId())


class PMShuttleEvent(wx.PyEvent):
    def __init__(self, position):
        wx.PyEvent.__init__(self)
        self.SetEventType (grEVT_POWERMATE_SHUTTLE)
        self.position = position

    def Clone (self): 
        self.__class__(self.GetId())
        
# ------------------------------------------------------------------------
#  Example usage
# ------------------------------------------------------------------------

if __name__ == '__main__':
    class Frame(wx.Frame):
        def __init__(self,parent=None,id=-1,title='Title',
                     pos=wx.DefaultPosition, size=(400,200)):
            wx.Frame.__init__(self,parent,id,title,pos,size)
            EVT_POWERMATE_BUTTON(self, self.on_button)
            EVT_POWERMATE_ROTATE(self, self.on_rotate)
            EVT_POWERMATE_SHUTTLE(self, self.on_shuttle)
            self.brightness = 128
            self.pulse_speed = 0
            
            try:
                self.pm = powermate(self)
            except:
                sys.stderr.write("Unable to find PowerMate or Contour Shuttle\n")
                sys.exit(1)

            self.pm.set_led_state(self.brightness, self.pulse_speed) 


        def on_button(self, evt):
            print "Button %d %s" % (evt.button,
                                    ("Released", "Pressed")[evt.value])

        def on_rotate(self, evt):
            print "Rotated %d" % (evt.delta,)
            if 0:
                new = max(0, min(255, self.brightness + evt.delta))
                if new != self.brightness:
                    self.brightness = new
                    self.pm.set_led_state(self.brightness, self.pulse_speed) 
        
        def on_shuttle(self, evt):
            print "Shuttle %d" % (evt.position,)
        
    class App(wx.App):
        def OnInit(self):
            title='PowerMate Demo'
            self.frame = Frame(parent=None,id=-1,title=title)
            self.frame.Show()
            self.SetTopWindow(self.frame)
            return True

    app = App()
    app.MainLoop ()