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