Imported Upstream version 3.2.2
[debian/gnuradio] / gr-wxgui / src / python / powermate.py
1 #!/usr/bin/env python
2 #
3 # Copyright 2005 Free Software Foundation, Inc.
4
5 # This file is part of GNU Radio
6
7 # GNU Radio is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3, or (at your option)
10 # any later version.
11
12 # GNU Radio is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16
17 # You should have received a copy of the GNU General Public License
18 # along with GNU Radio; see the file COPYING.  If not, write to
19 # the Free Software Foundation, Inc., 51 Franklin Street,
20 # Boston, MA 02110-1301, USA.
21
22
23 """
24 Handler for Griffin PowerMate, Contour ShuttlePro & ShuttleXpress USB knobs
25
26 This is Linux and wxPython specific.
27 """
28
29 import os
30 import sys
31 import struct
32 import exceptions
33 import threading
34 import wx
35 from gnuradio import gru
36
37 imported_ok = True
38
39 try:
40     import select
41     import fcntl
42 except ImportError:
43     imported_ok = False
44
45
46 # First a little bit of background:
47 #
48 # The Griffin PowerMate has
49 #  * a single knob which rotates
50 #  * a single button (pressing the knob)
51 #
52 # The Contour ShuttleXpress (aka SpaceShuttle) has
53 #  * "Jog Wheel"  -- the knob (rotary encoder) on the inside
54 #  * "Shuttle Ring" -- the spring loaded rubber covered ring
55 #  * 5 buttons
56 #
57 # The Contour ShuttlePro has
58 #  * "Jog Wheel" -- the knob (rotary encoder) on the inside
59 #  * "Shuttle Ring" -- the spring loaded rubber covered ring
60 #  * 13 buttons
61 #
62 # The Contour ShuttlePro V2 has
63 #  *"Jog Wheel" -- the knob (rotary encoder) on the inside
64 #  * "Shuttle Ring" -- the spring loaded rubber covered ring
65 #  * 15 buttons
66
67 # We remap all the buttons on the devices so that they start at zero.
68
69 # For the ShuttleXpress the buttons are 0 to 4 (left to right)
70
71 # For the ShuttlePro, we number the buttons immediately above
72 # the ring 0 to 4 (left to right) so that they match our numbering
73 # on the ShuttleXpress.  The top row is 5, 6, 7, 8.  The first row below
74 # the ring is 9, 10, and the bottom row is 11, 12.
75
76 # For the ShuttlePro V2, buttons 13 & 14 are to the
77 # left and right of the wheel respectively.
78
79 # We generate 3 kinds of events:
80 #
81 #   button press/release (button_number, press/release)
82 #   knob rotation (relative_clicks)       # typically -1, +1
83 #   shuttle position (absolute_position)  # -7,-6,...,0,...,6,7
84
85 # ----------------------------------------------------------------
86 # Our ID's for the devices:
87 # Not to be confused with anything related to magic hardware numbers.
88
89 ID_POWERMATE         = 'powermate'
90 ID_SHUTTLE_XPRESS    = 'shuttle xpress'
91 ID_SHUTTLE_PRO       = 'shuttle pro'
92 ID_SHUTTLE_PRO_V2    = 'shuttle pro v2'
93
94 # ------------------------------------------------------------------------
95 # format of messages that we read from /dev/input/event*
96 # See /usr/include/linux/input.h for more info
97 #
98 #struct input_event {
99 #        struct timeval time; = {long seconds, long microseconds}
100 #        unsigned short type;
101 #        unsigned short code;
102 #        unsigned int value;
103 #};
104
105 input_event_struct = "@llHHi"
106 input_event_size = struct.calcsize(input_event_struct)
107
108 # ------------------------------------------------------------------------
109 # input_event types
110 # ------------------------------------------------------------------------
111
112 IET_SYN           = 0x00   # aka RESET
113 IET_KEY           = 0x01   # key or button press/release
114 IET_REL           = 0x02   # relative movement (knob rotation)
115 IET_ABS           = 0x03   # absolute position (graphics pad, etc)
116 IET_MSC           = 0x04
117 IET_LED           = 0x11
118 IET_SND           = 0x12
119 IET_REP           = 0x14
120 IET_FF            = 0x15
121 IET_PWR           = 0x16
122 IET_FF_STATUS     = 0x17
123 IET_MAX           = 0x1f
124
125 # ------------------------------------------------------------------------
126 # input_event codes (there are a zillion of them, we only define a few)
127 # ------------------------------------------------------------------------
128
129 # these are valid for IET_KEY
130
131 IEC_BTN_0          = 0x100
132 IEC_BTN_1          = 0x101
133 IEC_BTN_2          = 0x102
134 IEC_BTN_3          = 0x103
135 IEC_BTN_4          = 0x104
136 IEC_BTN_5          = 0x105
137 IEC_BTN_6          = 0x106
138 IEC_BTN_7          = 0x107
139 IEC_BTN_8          = 0x108
140 IEC_BTN_9          = 0x109
141 IEC_BTN_10         = 0x10a
142 IEC_BTN_11         = 0x10b
143 IEC_BTN_12         = 0x10c
144 IEC_BTN_13         = 0x10d
145 IEC_BTN_14         = 0x10e
146 IEC_BTN_15         = 0x10f
147
148 # these are valid for IET_REL (Relative axes)
149
150 IEC_REL_X          = 0x00
151 IEC_REL_Y          = 0x01
152 IEC_REL_Z          = 0x02
153 IEC_REL_HWHEEL     = 0x06
154 IEC_REL_DIAL       = 0x07   # rotating the knob
155 IEC_REL_WHEEL      = 0x08   # moving the shuttle ring
156 IEC_REL_MISC       = 0x09
157 IEC_REL_MAX        = 0x0f
158
159 # ------------------------------------------------------------------------
160
161 class powermate(threading.Thread):
162     """
163     Interface to Griffin PowerMate and Contour Shuttles
164     """
165     def __init__(self, event_receiver=None, filename=None, **kwargs):
166         self.event_receiver = event_receiver
167         self.handle = -1
168         if not imported_ok:
169             raise exceptions.RuntimeError, 'powermate not supported on this platform'
170
171         if filename:
172             if not self._open_device(filename):
173                 raise exceptions.RuntimeError, 'Unable to find powermate'
174         else:
175             ok = False
176             for d in range(0, 16):
177                 if self._open_device("/dev/input/event%d" % d):
178                     ok = True
179                     break
180             if not ok:
181                 raise exceptions.RuntimeError, 'Unable to find powermate'
182
183         threading.Thread.__init__(self, **kwargs)
184         self.setDaemon (1)
185         self.keep_running = True
186         self.start ()
187         
188     def __del__(self):
189         self.keep_running = False
190         if self.handle >= 0:
191             os.close(self.handle)
192             self.handle = -1
193
194     def _open_device(self, filename):
195         try:
196             self.handle = os.open(filename, os.O_RDWR)
197             if self.handle < 0:
198                 return False
199
200             # read event device name
201             name = fcntl.ioctl(self.handle, gru.hexint(0x80ff4506), chr(0) * 256)
202             name = name.replace(chr(0), '')
203
204             # do we see anything we recognize?
205             if name == 'Griffin PowerMate' or name == 'Griffin SoundKnob':
206                 self.id = ID_POWERMATE
207                 self.mapper = _powermate_remapper()
208             elif name == 'CAVS SpaceShuttle A/V' or name == 'Contour Design ShuttleXpress':
209                 self.id = ID_SHUTTLE_XPRESS
210                 self.mapper = _contour_remapper()
211             elif name == 'Contour Design ShuttlePRO':
212                 self.id = ID_SHUTTLE_PRO
213                 self.mapper = _contour_remapper()
214             elif name == 'Contour Design ShuttlePRO v2':
215                 self.id = ID_SHUTTLE_PRO_V2
216                 self.mapper = _contour_remapper()
217             else:
218                 os.close(self.handle)
219                 self.handle = -1
220                 return False
221
222             # get exclusive control of the device, using ioctl EVIOCGRAB
223             # there may be an issue with this on non x86 platforms and if
224             # the _IOW,_IOC,... macros in <asm/ioctl.h> are changed
225             fcntl.ioctl(self.handle,gru.hexint(0x40044590), 1)
226             return True
227         except exceptions.OSError:
228             return False
229
230     
231     def set_event_receiver(self, obj):
232         self.event_receiver = obj
233
234
235     def set_led_state(self, static_brightness, pulse_speed=0,
236                       pulse_table=0, pulse_on_sleep=0, pulse_on_wake=0):
237         """
238         What do these magic values mean...
239         """
240         if self.id != ID_POWERMATE:
241             return False
242         
243         static_brightness &= 0xff;
244         if pulse_speed < 0:
245             pulse_speed = 0
246         if pulse_speed > 510:
247             pulse_speed = 510
248         if pulse_table < 0:
249             pulse_table = 0
250         if pulse_table > 2:
251             pulse_table = 2
252         pulse_on_sleep = not not pulse_on_sleep # not not = convert to 0/1
253         pulse_on_wake  = not not pulse_on_wake
254         magic = (static_brightness
255                  | (pulse_speed << 8)
256                  | (pulse_table << 17)
257                  | (pulse_on_sleep << 19)
258                  | (pulse_on_wake << 20))
259         data = struct.pack(input_event_struct, 0, 0, 0x04, 0x01, magic)
260         os.write(self.handle, data)
261         return True
262
263     def run (self):
264         while (self.keep_running):
265             s = os.read (self.handle, input_event_size)
266             if not s:
267                 self.keep_running = False
268                 break
269
270             raw_input_event = struct.unpack(input_event_struct,s)
271             sec, usec, type, code, val = self.mapper(raw_input_event)
272             
273             if self.event_receiver is None:
274                 continue
275             
276             if type == IET_SYN:    # ignore
277                 pass
278             elif type == IET_MSC:  # ignore (seems to be PowerMate reporting led brightness)
279                 pass                     
280             elif type == IET_REL and code == IEC_REL_DIAL:
281                 #print "Dial: %d" % (val,)
282                 wx.PostEvent(self.event_receiver, PMRotateEvent(val))
283             elif type == IET_REL and code == IEC_REL_WHEEL:
284                 #print "Shuttle: %d" % (val,)
285                 wx.PostEvent(self.event_receiver, PMShuttleEvent(val))
286             elif type == IET_KEY:
287                 #print "Key: Btn%d %d" % (code - IEC_BTN_0, val)
288                 wx.PostEvent(self.event_receiver,
289                              PMButtonEvent(code - IEC_BTN_0, val))
290             else:
291                 print "powermate: unrecognized event: type = 0x%x  code = 0x%x  val = %d" % (type, code, val)
292
293
294 class _powermate_remapper(object):
295     def __init__(self):
296         pass
297     def __call__(self, event):
298         """
299         Notice how nice and simple this is...
300         """
301         return event
302
303 class _contour_remapper(object):
304     def __init__(self):
305         self.prev = None
306     def __call__(self, event):
307         """
308         ...and how screwed up this is
309         """
310         sec, usec, type, code, val = event
311         if type == IET_REL and code == IEC_REL_WHEEL:
312             # === Shuttle ring ===
313             # First off, this really ought to be IET_ABS, not IET_REL!
314             # They never generate a zero value so you can't
315             # tell when the shuttle ring is back in the center.
316             # We kludge around this by calling both -1 and 1 zero.
317             if val == -1 or val == 1:
318                 return (sec, usec, type, code, 0)
319             return event
320
321         if type == IET_REL and code == IEC_REL_DIAL:
322             # === Jog knob (rotary encoder) ===
323             # Dim wits got it wrong again!  This one should return a
324             # a relative value, e.g., -1, +1.  Instead they return
325             # a total that runs modulo 256 (almost!).   For some
326             # reason they count like this 253, 254, 255, 1, 2, 3
327
328             if self.prev is None:                  # first time call
329                 self.prev = val
330                 return (sec, usec, IET_SYN, 0, 0)  # will be ignored above
331
332             diff = val - self.prev
333             if diff == 0:                          # sometimes it just sends stuff...
334                 return (sec, usec, IET_SYN, 0, 0)  # will be ignored above
335
336             if abs(diff) > 100:      # crossed into the twilight zone
337                 if self.prev > val:  # we've wrapped going forward
338                     self.prev = val
339                     return (sec, usec, type, code, +1)
340                 else:                # we've wrapped going backward
341                     self.prev = val
342                     return (sec, usec, type, code, -1)
343
344             self.prev = val
345             return (sec, usec, type, code, diff)
346
347         if type == IET_KEY:
348             # remap keys so that all 3 gadgets have buttons 0 to 4 in common
349             return (sec, usec, type, 
350                     (IEC_BTN_5, IEC_BTN_6, IEC_BTN_7, IEC_BTN_8,
351                      IEC_BTN_0, IEC_BTN_1, IEC_BTN_2, IEC_BTN_3, IEC_BTN_4,
352                      IEC_BTN_9,  IEC_BTN_10,
353                      IEC_BTN_11, IEC_BTN_12,
354                      IEC_BTN_13, IEC_BTN_14)[code - IEC_BTN_0], val)
355             
356         return event
357
358 # ------------------------------------------------------------------------
359 # new wxPython event classes
360 # ------------------------------------------------------------------------
361
362 grEVT_POWERMATE_BUTTON  = wx.NewEventType()
363 grEVT_POWERMATE_ROTATE  = wx.NewEventType()
364 grEVT_POWERMATE_SHUTTLE = wx.NewEventType()
365
366 EVT_POWERMATE_BUTTON = wx.PyEventBinder(grEVT_POWERMATE_BUTTON, 0)
367 EVT_POWERMATE_ROTATE = wx.PyEventBinder(grEVT_POWERMATE_ROTATE, 0)
368 EVT_POWERMATE_SHUTTLE = wx.PyEventBinder(grEVT_POWERMATE_SHUTTLE, 0)
369
370 class PMButtonEvent(wx.PyEvent):
371     def __init__(self, button, value):
372         wx.PyEvent.__init__(self)
373         self.SetEventType(grEVT_POWERMATE_BUTTON)
374         self.button = button
375         self.value = value
376
377     def Clone (self): 
378         self.__class__(self.GetId())
379         
380
381 class PMRotateEvent(wx.PyEvent):
382     def __init__(self, delta):
383         wx.PyEvent.__init__(self)
384         self.SetEventType (grEVT_POWERMATE_ROTATE)
385         self.delta = delta
386
387     def Clone (self): 
388         self.__class__(self.GetId())
389
390
391 class PMShuttleEvent(wx.PyEvent):
392     def __init__(self, position):
393         wx.PyEvent.__init__(self)
394         self.SetEventType (grEVT_POWERMATE_SHUTTLE)
395         self.position = position
396
397     def Clone (self): 
398         self.__class__(self.GetId())
399         
400 # ------------------------------------------------------------------------
401 #  Example usage
402 # ------------------------------------------------------------------------
403
404 if __name__ == '__main__':
405     class Frame(wx.Frame):
406         def __init__(self,parent=None,id=-1,title='Title',
407                      pos=wx.DefaultPosition, size=(400,200)):
408             wx.Frame.__init__(self,parent,id,title,pos,size)
409             EVT_POWERMATE_BUTTON(self, self.on_button)
410             EVT_POWERMATE_ROTATE(self, self.on_rotate)
411             EVT_POWERMATE_SHUTTLE(self, self.on_shuttle)
412             self.brightness = 128
413             self.pulse_speed = 0
414             
415             try:
416                 self.pm = powermate(self)
417             except:
418                 sys.stderr.write("Unable to find PowerMate or Contour Shuttle\n")
419                 sys.exit(1)
420
421             self.pm.set_led_state(self.brightness, self.pulse_speed) 
422
423
424         def on_button(self, evt):
425             print "Button %d %s" % (evt.button,
426                                     ("Released", "Pressed")[evt.value])
427
428         def on_rotate(self, evt):
429             print "Rotated %d" % (evt.delta,)
430             if 0:
431                 new = max(0, min(255, self.brightness + evt.delta))
432                 if new != self.brightness:
433                     self.brightness = new
434                     self.pm.set_led_state(self.brightness, self.pulse_speed) 
435         
436         def on_shuttle(self, evt):
437             print "Shuttle %d" % (evt.position,)
438         
439     class App(wx.App):
440         def OnInit(self):
441             title='PowerMate Demo'
442             self.frame = Frame(parent=None,id=-1,title=title)
443             self.frame.Show()
444             self.SetTopWindow(self.frame)
445             return True
446
447     app = App()
448     app.MainLoop ()