f20dfda359d6210488481b95ca4a0925c49e601f
[openpandora.oe.git] / recipes / pandora-system / pandora-scripts / op_nubmode.py
1 #!/usr/bin/python
2
3 # TODO: 
4 # - fix nub reset (permission issue) and add test
5 # - wrap into PND
6 # - upload into beta software, gather & process feedback
7 # - upload into repo
8
9 import os
10 import re
11 import sys
12 import gtk
13 import time
14 import optparse
15
16 # EDs reset in op_nubmode.sh
17 #
18 # @left-nub: 3-0066/reset
19
20 #      echo 1 > /sys/bus/i2c/drivers/vsense/3-0067/reset
21 #        sleep 1
22 #        echo 0 > /sys/bus/i2c/drivers/vsense/3-0067/reset
23 #        curmode=$(cat /proc/pandora/nub1/mode)
24 #        echo mouse > /proc/pandora/nub1/mode
25 #        while ! zenity --question --title="Resetted right nub" --text="The right nub has been resetted.\nPlease try to move the mouse cursor\nto test if it is working properly." --ok-label="Working properly" --cancel-label="Reset again"; do
26 #        echo 1 > /sys/bus/i2c/drivers/vsense/3-0067/reset
27 #        sleep 1
28 #        echo 0 > /sys/bus/i2c/drivers/vsense/3-0067/reset      
29 #        done
30 #        echo $curmode > /proc/pandora/nub1/mode
31
32 # ================================================================
33
34 GUI_DESCRIPTION = 'nubmode.glade'
35 PROFILES = '/etc/pandora/conf/nub_profiles.conf'
36
37 MODES = ("mouse", "mbuttons", "scroll", "absolute")
38 SLIDERS = ("mouse", "button", "scroll", "scrollx", "scrolly")
39
40 DEFAULT_PROFILENAME = "Default"
41 DEFAULT_PROFILE = [DEFAULT_PROFILENAME, 
42                    "mouse 150 20 20 7 7", "mbuttons 150 20 20 7 7"]
43
44 RESET_CMD_LEFT = 'echo %i > /sys/bus/i2c/drivers/vsense/3-0066/reset'
45 RESET_CMD_RIGHT = 'echo %i > /sys/bus/i2c/drivers/vsense/3-0067/reset'
46
47 # Settings read/written to /proc/pandora/nub<x> (<x>: 0 or 1)
48 FILES = "mode mouse_sensitivity mbutton_threshold scroll_rate scrollx_sensitivity scrolly_sensitivity".split(' ')
49 LEFT_NUB_CONFIG = [os.path.join('/proc/pandora/nub0', x) for x in FILES]
50 RIGHT_NUB_CONFIG = [os.path.join('/proc/pandora/nub1', x) for x in FILES]
51
52 RE_FORMAT = "(%s),(\d+),(\d+),(\d+),(\d+),(\d+)" % '|'.join(MODES)
53 BOUNDS = ((50, 300), (1, 40), (1, 40), (-32, 32), (-32, 32))
54
55 # ================================================================
56
57 def Validate(value):
58     mo = re.match(RE_FORMAT, value)
59     if mo and all(lower <= int(value) <= upper for (value, (lower, upper)) in
60                   zip(mo.groups()[1:], BOUNDS)):
61         return True, list(mo.groups())
62     else:
63         return False, None
64
65 def ReadConfigFromProc(paths):
66     config = []
67     for filepath in paths:
68         with open(filepath) as f:
69             config.append(f.readline().strip())
70     return config
71
72 def StoreConfigToProc(paths, values):
73     # fix for scrollx, scrolly being stored one closer to zero than specified    
74     values[-2:] = [int(v) + (-1 if v < 0 else 1) for v in values[-2:]]
75
76     for filepath, value in zip(paths, values):
77         with open(filepath, 'w') as f:
78             f.write('%s\n' % value)
79
80
81 class Nub(object):
82     def __init__(self, builder, modeprefix, sliderprefix):
83         self.radios = [builder.get_object(modeprefix % m) for m in MODES]
84         self.sliders = [builder.get_object(sliderprefix % s) for s in SLIDERS]
85
86     def SetConfig(self, config):
87         i = MODES.index(config[0])
88         self.radios[i].set_active(True)
89         for s, v in zip(self.sliders, config[1:]):
90             s.value = int(v)
91
92     def GetConfig(self):
93         i = (j for j, r in enumerate(self.radios) if r.get_active()).next()
94         config = [MODES[i]]
95         for s in self.sliders:
96             config.append(str(int(s.value)))
97         return config
98
99
100 class NubConfig(object):
101     """GUI application for modifying the pandora nub configuration"""
102     def __init__(self):
103         builder = gtk.Builder()
104         builder.add_from_file(
105             os.path.join(os.path.dirname(__file__), GUI_DESCRIPTION))
106         builder.connect_signals(self)
107
108         self.leftnub = Nub(builder, "LeftRadio_%s", "l%s")
109         self.rightnub = Nub(builder, "RightRadio_%s", "r%s")
110
111         self.profiles = gtk.ListStore(str, str, str)
112         self.profiles.append(DEFAULT_PROFILE)
113         with open(PROFILES) as f:
114             c = map(str.rstrip, f.readlines())
115             for p in zip(*(c[i::3] for i in range(3))):
116                 self.profiles.append(p)
117
118         self.statusbar = builder.get_object("statusbar")
119         self.contextid = self.statusbar.get_context_id('')
120
121         self.comboentry = builder.get_object("ProfileComboEntry")
122         self.comboentry.set_model(self.profiles)
123         self.comboentry.set_text_column(0)
124
125         self.entry = self.comboentry.get_child()
126         self.entry.connect('changed', self.on_combo_changed)
127
128         self.load = builder.get_object('LoadProfile')
129         self.save = builder.get_object('SaveProfile')
130
131         self.comboentry.set_active(0)
132
133         # read current config:
134         self.on_UndoChanges_clicked(None)
135
136         self.window = builder.get_object("window")
137         self.window.connect("destroy", self.on_window_destroy)
138         self.window.show_all()
139
140         accelgrp = gtk.AccelGroup()
141         key, mod = gtk.accelerator_parse('<Control>Q')
142         accelgrp.connect_group(key, mod, 0, self.on_window_destroy)
143         self.window.add_accel_group(accelgrp)        
144
145     def write_profiles_to_file(self):
146         with open(PROFILES, 'w') as f:
147             for name,left,right in self.profiles:                            
148                 if name != DEFAULT_PROFILENAME:
149                     f.write("%s\n%s\n%s\n" % (name, left, right))
150
151     def Notify(self, message):
152         self.statusbar.pop(self.contextid)
153         self.statusbar.push(self.contextid, message)
154
155     def on_combo_changed(self, widget, *data):
156         profileid = self.comboentry.get_active()
157         self.load.set_sensitive(profileid != -1)
158         self.save.set_sensitive(profileid != 0 and self.entry.get_text() != '')
159
160     def on_ResetLeft_clicked(self, widget, *data):
161         self.Notify("Resetting left nub...")
162         os.system(RESET_CMD_LEFT % 1)
163         time.sleep(1)
164         os.system(RESET_CMD_LEFT % 0)
165         self.Notify("Left nub reset")
166
167     def on_ResetRight_clicked(self, widget, *data):
168         self.Notify("Resetting right nub...")
169         os.system(RESET_CMD_RIGHT % 1)
170         time.sleep(1)
171         os.system(RESET_CMD_RIGHT % 0)
172         self.Notify("Right nub reset")
173
174     def on_UndoChanges_clicked(self, widget, *data):
175         self.leftnub.SetConfig(ReadConfigFromProc(LEFT_NUB_CONFIG))
176         self.rightnub.SetConfig(ReadConfigFromProc(RIGHT_NUB_CONFIG))
177         self.Notify("Active nub configuration loaded.")
178
179     def on_ApplyChanges_clicked(self, widget, *data):
180         StoreConfigToProc(LEFT_NUB_CONFIG, self.leftnub.GetConfig())
181         StoreConfigToProc(RIGHT_NUB_CONFIG, self.rightnub.GetConfig())
182         self.Notify("Nub configuration updated.")
183
184     def on_SaveProfile_clicked(self, widget, *data):
185         name = self.entry.get_text().replace(' ', '_')
186         if name == '' or name == DEFAULT_PROFILENAME:
187             self.Notify("Invalid profile name")
188         else:
189             left = ' '.join(self.leftnub.GetConfig())
190             right = ' '.join(self.rightnub.GetConfig())
191             for profileid, row in enumerate(self.profiles):
192                 if row[0] == name:
193                     row[1] = left
194                     row[2] = right
195                     self.comboentry.set_active(profileid)
196                     break
197             else:
198                 self.profiles.append([name, left, right])
199                 self.entry.set_text(name)
200             self.Notify("Profile saved as: %s" % name)
201
202     def on_LoadProfile_clicked(self, widget, *data):
203         profileid = self.comboentry.get_active()
204         if profileid == -1:
205             self.Notify("Cannot load profile, please select an existing profile.")
206         else:
207             name, left, right = self.profiles[profileid]
208             self.leftnub.SetConfig(left.split(' '))
209             self.rightnub.SetConfig(right.split(' '))
210             self.Notify("Profile loaded, hit 'Apply configuration' to make it active")
211         
212     def on_window_destroy(self, widget, *data):
213         self.Notify("Storing profiles...")
214         self.write_profiles_to_file()
215         gtk.main_quit()
216
217 if __name__ == '__main__':
218     parser = optparse.OptionParser()
219     parser.add_option('--reset', default='',
220         help="Reset specified nub(s). Format: left,right")
221     parser.add_option('-l', '--left_nub', default='',
222         help="Configure left nub. Format: %s. E.g. mouse,150,20,20,7,7" % ' '.join(FILES).replace(' ', ','))
223     parser.add_option('-r', '--right_nub', default='',
224         help="Configure right nub. Format: %s. E.g. mbuttons,150,20,20,7,7" % ' '.join(FILES).replace(' ', ','))
225     parser.add_option('-s', '--save_profile', default='',
226         help="Store current configuration as specified profile (no spaces allowed)")
227     parser.add_option('-p', '--load_profile', default='',
228         help="Load specified nub configuration profile")
229     options, args = parser.parse_args()
230
231     app = NubConfig()
232     if len(sys.argv) == 1: # run gui app
233         gtk.main()
234     else: # run command line app
235         if 'left' in options.reset:
236             app.on_ResetLeft_clicked(None)
237         if 'right' in options.reset:
238             app.on_ResetRight_clicked(None)
239         ok, values = Validate(options.left_nub)
240         if ok: 
241             StoreConfigToProc(LEFT_NUB_CONFIG, values)
242         ok, values = Validate(options.right_nub)
243         if ok:
244             StoreConfigToProc(RIGHT_NUB_CONFIG, values)
245         if options.save_profile:
246             app.on_UndoChanges_clicked(None)
247             app.entry.set_text(options.save_profile)
248             app.on_SaveProfile_clicked(None)
249             app.write_profiles_to_file()
250         if options.load_profile:
251             for profileid, row in enumerate(app.profiles):
252                 if row[0] == options.load_profile:
253                     app.comboentry.set_active(profileid)
254                     app.on_LoadProfile_clicked(None)
255                     app.on_ApplyChanges_clicked(None)