4 # - fix nub reset (permission issue) and add test
6 # - upload into beta software, gather & process feedback
16 # EDs reset in op_nubmode.sh
18 # @left-nub: 3-0066/reset
20 # echo 1 > /sys/bus/i2c/drivers/vsense/3-0067/reset
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
28 # echo 0 > /sys/bus/i2c/drivers/vsense/3-0067/reset
30 # echo $curmode > /proc/pandora/nub1/mode
32 # ================================================================
34 GUI_DESCRIPTION = 'nubmode.glade'
35 PROFILES = '/etc/pandora/conf/nub_profiles.conf'
37 MODES = ("mouse", "mbuttons", "scroll", "absolute")
38 SLIDERS = ("mouse", "button", "scroll", "scrollx", "scrolly")
40 DEFAULT_PROFILENAME = "Default"
41 DEFAULT_PROFILE = [DEFAULT_PROFILENAME,
42 "mouse 150 20 20 7 7", "mbuttons 150 20 20 7 7"]
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'
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]
52 RE_FORMAT = "(%s),(\d+),(\d+),(\d+),(\d+),(\d+)" % '|'.join(MODES)
53 BOUNDS = ((50, 300), (1, 40), (1, 40), (-32, 32), (-32, 32))
55 # ================================================================
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())
65 def ReadConfigFromProc(paths):
67 for filepath in paths:
68 with open(filepath) as f:
69 config.append(f.readline().strip())
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:]]
76 for filepath, value in zip(paths, values):
77 with open(filepath, 'w') as f:
78 f.write('%s\n' % value)
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]
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:]):
93 i = (j for j, r in enumerate(self.radios) if r.get_active()).next()
95 for s in self.sliders:
96 config.append(str(int(s.value)))
100 class NubConfig(object):
101 """GUI application for modifying the pandora nub configuration"""
103 builder = gtk.Builder()
104 builder.add_from_file(
105 os.path.join(os.path.dirname(__file__), GUI_DESCRIPTION))
106 builder.connect_signals(self)
108 self.leftnub = Nub(builder, "LeftRadio_%s", "l%s")
109 self.rightnub = Nub(builder, "RightRadio_%s", "r%s")
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)
118 self.statusbar = builder.get_object("statusbar")
119 self.contextid = self.statusbar.get_context_id('')
121 self.comboentry = builder.get_object("ProfileComboEntry")
122 self.comboentry.set_model(self.profiles)
123 self.comboentry.set_text_column(0)
125 self.entry = self.comboentry.get_child()
126 self.entry.connect('changed', self.on_combo_changed)
128 self.load = builder.get_object('LoadProfile')
129 self.save = builder.get_object('SaveProfile')
131 self.comboentry.set_active(0)
133 # read current config:
134 self.on_UndoChanges_clicked(None)
136 self.window = builder.get_object("window")
137 self.window.connect("destroy", self.on_window_destroy)
138 self.window.show_all()
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)
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))
151 def Notify(self, message):
152 self.statusbar.pop(self.contextid)
153 self.statusbar.push(self.contextid, message)
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() != '')
160 def on_ResetLeft_clicked(self, widget, *data):
161 self.Notify("Resetting left nub...")
162 os.system(RESET_CMD_LEFT % 1)
164 os.system(RESET_CMD_LEFT % 0)
165 self.Notify("Left nub reset")
167 def on_ResetRight_clicked(self, widget, *data):
168 self.Notify("Resetting right nub...")
169 os.system(RESET_CMD_RIGHT % 1)
171 os.system(RESET_CMD_RIGHT % 0)
172 self.Notify("Right nub reset")
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.")
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.")
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")
189 left = ' '.join(self.leftnub.GetConfig())
190 right = ' '.join(self.rightnub.GetConfig())
191 for profileid, row in enumerate(self.profiles):
195 self.comboentry.set_active(profileid)
198 self.profiles.append([name, left, right])
199 self.entry.set_text(name)
200 self.Notify("Profile saved as: %s" % name)
202 def on_LoadProfile_clicked(self, widget, *data):
203 profileid = self.comboentry.get_active()
205 self.Notify("Cannot load profile, please select an existing profile.")
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")
212 def on_window_destroy(self, widget, *data):
213 self.Notify("Storing profiles...")
214 self.write_profiles_to_file()
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()
232 if len(sys.argv) == 1: # run gui app
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)
241 StoreConfigToProc(LEFT_NUB_CONFIG, values)
242 ok, values = Validate(options.right_nub)
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)