omap3-pandora-kernel2: update
[openpandora.oe.git] / recipes / pandora-system / pandora-scripts / op_nubmode.py
1 #!/usr/bin/python
2
3 import os
4 import re
5 import sys
6 import gtk
7 import time
8 import optparse
9 import shutil
10
11 # ================================================================
12
13 GUI_DESCRIPTION = 'nubmode.glade'
14 PROFILES = '/etc/pandora/conf/nub_profiles.conf'
15
16 # Shell command to reset nub: 3-0066 = left-nub, 3-0067 = right-nub
17 # apparently they are linked and resetting one resets both.
18 #RESET_CMD = 'echo %i > /sys/bus/i2c/drivers/vsense/3-00%i/reset'
19 RESET_CMD = "sudo /usr/pandora/scripts/reset_nubs.sh"
20
21 # Valid values for mode setting
22 MODES = ("mouse", "mbuttons", "scroll", "absolute")
23
24 # Paths for reading and writing different configuration options %i -> 0 | 1
25 SETTINGS = dict(
26     mode='/proc/pandora/nub%s/mode',
27     mouse='/proc/pandora/nub%s/mouse_sensitivity',
28     button='/proc/pandora/nub%s/mbutton_threshold',
29     rate='/proc/pandora/nub%s/scroll_rate',
30     scrollx='/proc/pandora/nub%s/scrollx_sensitivity',
31     scrolly='/proc/pandora/nub%s/scrolly_sensitivity',
32 )
33
34 # format for saving/loading
35 FILE_ORDER = ("mode", "mouse", "button", "rate", "scrollx", "scrolly")
36
37 # Default configuration
38 DEFAULT_PROFILENAME = "Default"
39 DEFAULT_DICT = dict(mode0='mouse', mode1='mbuttons', mouse0='150', mouse1='150',
40                     button0='20', button1='20', rate0='20', rate1='20',
41                     scrollx0='7', scrollx1='7', scrolly0='7', scrolly1='7')
42
43 # Types of model changes for view updates 
44 (MODEL_PROFILE_CHANGE, MODEL_VALUE_CHANGE) = range(2)
45
46 # Documentation for commandline options
47 HELP_RESET = "Reset specified nub(s). Format: left,right"
48 HELP_LEFT_NUB = ("Configure left nub. Include -a flag to activate. Format: %s. E.g. %s" %
49                  (' '.join(FILE_ORDER),
50                   ' '.join(DEFAULT_DICT[k+'0'] for k in FILE_ORDER)))
51 HELP_RIGHT_NUB = ("Configure right nub. Include -a flag to activate. Format: %s. E.g. %s" %
52                   (' '.join(FILE_ORDER),
53                    ' '.join(DEFAULT_DICT[k+'1'] for k in FILE_ORDER)))
54 HELP_SAVE = "Store current configuration as specified profile (no spaces allowed)"
55 HELP_APPLY = "Write currently loaded configuration to nubs."
56 HELP_LOAD = "Load and apply specified nub configuration profile"
57 HELP_DEL = "Delete specified nub configuration profile"
58
59 # Commandline input type checking
60 TYPECHECK = dict(mode='(%s)' % '|'.join(MODES), 
61                  mouse='(\d+)', button='(\d+)',
62                  rate='(\d+)', scrollx='(\d+)', scrolly='(\d+)')
63 RE_FORMAT = ','.join(TYPECHECK[k] for k in FILE_ORDER)
64 BOUNDS = dict(mouse=(50, 300), button=(1, 40), rate=(1, 40),
65               scrollx=(-32,32), scrolly=(-32,32))
66
67 # ================================================================
68 # There is a bug in setting scrollx/scrolly sensitivity in the
69 # firmware (to be fixed in hotfix 6). 
70 # Detect and set FIX_SCROLLXY_BUG to use a workaround.
71
72 def ReadWriteTest(value=None):
73     with open(SETTINGS['scrollx'] % 0, 'w' if value else 'r') as f:
74         return f.write('%s\n' % value) if value else f.readline().rstrip()
75
76 tmp = int(ReadWriteTest())                       # backup original
77 ReadWriteTest(tmp + (-1 if tmp < 0 else 1))      # write corrected value
78 FIX_SCROLLXY_BUG = int(ReadWriteTest()) == tmp   # fix bug if equal to original
79 ReadWriteTest(tmp + ((-1 if tmp < 0 else 1) if FIX_SCROLLXY_BUG else 0)) # restore
80
81 # ================================================================
82
83 def ReadProc():
84     config = {}
85     for key, value in SETTINGS.iteritems():
86         for c in '01':
87             with open(value % c, 'r') as f:
88                 config[key+c] = f.readline().strip()
89     return config
90
91 def StoreProc(dictionary):
92     # fix for value decrement/increment after written
93     if FIX_SCROLLXY_BUG:
94         dictionary = dictionary.copy()
95         for key in ('scrollx0', 'scrolly0', 'scrollx1', 'scrolly1'):
96             value = int(dictionary[key])
97             value += -1 if value < 0 else 1
98             dictionary[key] = str(value)
99     for key, value in SETTINGS.iteritems():
100         for c in '01':
101             with open(value % c, 'w') as f:
102                 f.write('%s\n' % dictionary[key+c])
103
104 def ProfileToString(dictionary):
105     return ' '.join(dictionary[k+c] for c in '01' for k in FILE_ORDER)
106
107 def StringToProfile(line):
108     return dict(zip((k+c for c in '01' for k in FILE_ORDER), line.split(' ')))
109
110 def Validate(value):
111     mo = re.match(RE_FORMAT, value)
112     if mo: # verify bounds
113         values = dict(zip(FILE_ORDER, mo.groups()))
114         if all(BOUNDS[k][0] <= int(values[k]) <= BOUNDS[k][1] for k in BOUNDS):
115             return values
116     return {}
117
118 # ================================================================
119
120 class NubModel(object):
121     def __init__(self, view=None):
122         self.views = [view] if view else []
123         self.settings = ReadProc()
124         self.profiles = {}
125         with open(PROFILES) as f:
126             name = None
127             for line in f:
128                 if name is None:
129                     name = line.rstrip()
130                 else:
131                     self.profiles[name] = StringToProfile(line.rstrip())
132                     name = None
133
134     def notify(self, reason, *args):
135         for v in self.views:
136             v.update_view(reason, *args)
137
138     def set_profile(self, name, dictionary):
139         notify = name not in self.profiles
140         self.profiles.setdefault(name, {}).update(dictionary)
141         if notify:
142             self.notify(MODEL_PROFILE_CHANGE, name, self.profiles)
143
144     def delete_profile(self, name):
145         notify = name in self.profiles
146         del self.profiles[name]
147         if notify:
148             self.notify(MODEL_PROFILE_CHANGE, '', self.profiles)
149
150     def load_profile(self, settings):
151         self.settings.update(settings)
152         self.notify(MODEL_VALUE_CHANGE, self.settings)
153
154     def load_named_profile(self, name):
155         notify = True
156         if name == DEFAULT_PROFILENAME:
157             self.settings.update(DEFAULT_DICT)
158         elif name in self.profiles:
159             self.settings.update(self.profiles[name])
160         else:
161             notify = False
162         if notify:
163             self.notify(MODEL_VALUE_CHANGE, self.settings)
164
165     def store_profiles(self, filename):
166         with open(filename, 'w') as f:
167             for name in sorted(self.profiles.keys()):
168                 f.write('%s\n%s\n' % (name, ProfileToString(self.profiles[name])))
169
170
171 class NubConfig(object):
172     """GUI application for modifying the pandora nub configuration"""
173     def __init__(self):
174         builder = gtk.Builder()
175         builder.add_from_file(
176             os.path.join(os.path.dirname(__file__), GUI_DESCRIPTION))
177         builder.connect_signals(self)
178
179         # slider widgets, more specifically: their Adjustment objects
180         self.widgets = {}
181         for s in DEFAULT_DICT:
182             w = builder.get_object(s)
183             if w is not None:
184                 w.connect('value-changed', self.on_slider_changed, s)
185                 self.widgets[s] = w
186
187         # radio buttons
188         for c in '01':
189             group = []
190             for m in MODES:
191                 w = builder.get_object('R%s%s' % (m,c))
192                 w.connect('clicked', self.on_radio_changed, 'mode'+c, m)
193                 group.append(w)
194             self.widgets['mode'+c] = group
195
196         self.statusbar = builder.get_object('statusbar')
197         self.contextid = self.statusbar.get_context_id('')
198
199         self.model = NubModel(self)
200
201         self.profiles = gtk.ListStore(str)
202         self.profiles.append([DEFAULT_PROFILENAME])
203         for name in self.model.profiles:
204             self.profiles.append([name])
205
206         self.comboentry = builder.get_object('ProfileComboEntry')
207         self.comboentry.set_model(self.profiles)
208         self.comboentry.set_text_column(0)
209         self.comboentry.connect('changed', self.on_combo_changed)
210
211         self.entry = self.comboentry.get_child()
212         self.entry.connect('activate', self.on_LoadProfile_clicked)
213
214         # we need a reference to change their sensitive
215         # setting according to the profile
216         self.delete = builder.get_object('DeleteProfile')
217         self.save = builder.get_object('SaveProfile')
218
219         # this also changes sensitive of self.save & self.load
220         self.comboentry.set_active(0)
221
222         # read current config
223         self.model.load_profile(ReadProc())
224
225         self.window = builder.get_object("window")
226         self.window.show_all()
227
228         accelgrp = gtk.AccelGroup()
229         key, mod = gtk.accelerator_parse('<Control>Q')
230         accelgrp.connect_group(key, mod, 0, self.on_window_destroy)
231         self.window.add_accel_group(accelgrp)
232
233     def _update_profile_list(self, new, profiles):
234         self.profiles.clear()
235         self.profiles.append([DEFAULT_PROFILENAME])
236         activate = 0
237         for i, key in enumerate(sorted(profiles.keys())):
238             self.profiles.append([key])
239             if key == new:
240                 activate = i
241         self.comboentry.set_active(activate)
242             
243     def _update_widgets(self, settings):
244         for key, value in settings.iteritems():
245             if 'mode' in key:
246                 for w, m in zip(self.widgets[key], MODES):
247                     w.set_active(m == value)
248             else:
249                 self.widgets[key].value = int(value)
250
251     def update_view(self, reason, *data):
252         if reason == MODEL_PROFILE_CHANGE:
253             self._update_profile_list(*data)
254         elif reason == MODEL_VALUE_CHANGE:
255             self._update_widgets(*data)
256
257     def _lose_active(self):
258         # Forces the comboboxentry to lose its active selection
259         # such that it also generates a changed signal when we
260         # select the same value.
261         temp = self.entry.get_text()
262         self.entry.set_text('')
263         self.entry.set_text(temp)
264
265     def Notify(self, message):
266         self.statusbar.pop(self.contextid)
267         self.statusbar.push(self.contextid, message)
268
269     def on_slider_changed(self, widget, key):
270         self.model.settings[key] = str(int(widget.value))
271
272     def on_radio_changed(self, widget, key, mode):
273         self.model.settings[key] = mode
274
275     def on_combo_changed(self, widget, *data):
276         name = self.entry.get_text()
277         self.delete.set_sensitive(name in self.model.profiles)
278         self.save.set_sensitive(name != '' and name != DEFAULT_PROFILENAME)
279         if self.comboentry.get_active() != -1:
280             self.on_LoadProfile_clicked(None)
281
282     def on_ResetNubs_clicked(self, widget, *data):
283         self.Notify("Resetting the nubs...")
284         os.system(RESET_CMD)
285         self.Notify("Nubs reset.")
286
287     def on_ReadNubConfig_clicked(self, widget, *data):
288         self.model.load_profile(ReadProc())
289         self.Notify("Active nub configuration loaded.")
290
291     def on_WriteNubConfig_clicked(self, widget, *data):
292         StoreProc(self.model.settings)
293         shutil.copyfile("/etc/pandora/conf/nubs.state", "/tmp/nubs.state");
294         os.system("sed -i -e '1c\%s' /tmp/nubs.state" % self.model.settings['mode0'])
295         os.system("sed -i -e '7c\%s' /tmp/nubs.state" % self.model.settings['mode1'])
296         shutil.copyfile("/tmp/nubs.state", "/etc/pandora/conf/nubs.state");
297         self.Notify("Nub configuration updated.")
298
299     def on_SaveProfile_clicked(self, widget, *data):
300         name = self.entry.get_text()
301         if name == '' or name == DEFAULT_PROFILENAME:
302             self.Notify("Invalid profile name")
303         else:
304             self.model.set_profile(name, self.model.settings)
305             self.Notify("Profile saved as: %s" % name)
306
307     def on_LoadProfile_clicked(self, widget, *data):
308         self._lose_active()          # force widget to always emit changed signals,
309         name = self.entry.get_text() # ok since we always lookup by text value anyway
310         if not name:
311             return
312         elif name not in self.model.profiles and name != DEFAULT_PROFILENAME:
313             self.Notify("Cannot load profile, please select an existing profile.")
314         else:
315             self.model.load_named_profile(name)
316             self.Notify("Profile loaded, hit 'Write nub settings' to make it active")
317
318     def on_DeleteProfile_clicked(self, widget, *data):
319         name = self.entry.get_text()
320         if name not in self.model.profiles or name == DEFAULT_PROFILENAME:
321             self.model("Cannot remove profile, please select an existing non-default profile.")
322         else:
323             dialog = gtk.MessageDialog(flags=gtk.DIALOG_MODAL, type=gtk.MESSAGE_WARNING,
324                  buttons=gtk.BUTTONS_YES_NO,
325                  message_format="Are you sure you want to delete profile %s?" % name)
326             if dialog.run() == gtk.RESPONSE_YES:
327                 self.model.delete_profile(name)
328             dialog.destroy()
329         
330     def on_window_destroy(self, widget, *data):
331         self.Notify("Storing profiles...")
332         self.model.store_profiles(PROFILES)
333         gtk.main_quit()
334
335 # ================================================================
336
337 if __name__ == '__main__':
338     parser = optparse.OptionParser()
339     parser.add_option('--reset', default=False, action='store_true', help=HELP_RESET)
340     parser.add_option('-l', '--left_nub', default='', help=HELP_LEFT_NUB)
341     parser.add_option('-r', '--right_nub', default='', help=HELP_RIGHT_NUB)
342     parser.add_option('-s', '--save_profile', default='', help=HELP_SAVE)
343     parser.add_option('-a', '--apply', default=False, action='store_true', help=HELP_APPLY)
344     parser.add_option('-p', '--load_profile', default='', help=HELP_LOAD)
345     parser.add_option('-d', '--remove_profile', default='', help=HELP_DEL)
346     options, args = parser.parse_args()
347
348     if len(sys.argv) == 1: # no params: run gui app
349         app = NubConfig()
350         gtk.main()
351     else: # run command line app
352         model = NubModel()
353
354         if options.reset:
355             os.system(RESET_CMD)
356         for key, value in Validate(options.left_nub).iteritems():
357             model.settings[key+'0'] = value
358         for key, value in Validate(options.right_nub).iteritems():
359             model.settings[key+'1'] = value
360         if options.save_profile and options.save_profile != DEFAULT_PROFILENAME:
361             model.set_profile(options.save_profile, model.settings)
362         if options.apply:
363             StoreProc(model.settings)
364         if options.load_profile:
365             model.load_named_profile(options.load_profile)
366             StoreProc(model.settings)
367         if options.remove_profile:
368             model.delete_profile(options.remove_profile)
369         if options.save_profile or options.remove_profile:
370             model.store_profiles(PROFILES)