Find script for gsm
[gregoa/zavai.git] / src / input.vala
1 /*
2  * devinput - zavai /dev/input device handling
3  *
4  * Copyright (C) 2009  Enrico Zini <enrico@enricozini.org>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  */
20
21 namespace zavai {
22 namespace input {
23
24 public class DevInput : zavai.Service
25 {
26     public string device { get; construct; }
27
28     public signal bool event(LinuxInput.Event* ev);
29
30     protected IOChannel fd = null;
31     protected uint fd_watch = 0;
32
33     public DevInput(string name, string device)
34     {
35         Object(name: "input.power_button", device: "/dev/input/event0");
36     }
37
38     protected void close_fd()
39     {
40         if (fd != null)
41         {
42             try {
43                 fd.shutdown(false);
44             } catch (IOChannelError e) {
45                 zavai.log.error("When closing " + device + ": " + e.message);
46             }
47
48             fd = null;
49         }
50     }
51
52     protected bool on_input_data(IOChannel source, IOCondition condition)
53     {
54         if (condition != IOCondition.IN) return true;
55
56         //stderr.printf("GOT INPUT ON %s %d\n", device, source.unix_get_fd());
57         char[] buf = new char[sizeof(LinuxInput.Event)];
58         size_t count_read;
59         try {
60             source.read_chars(buf, out count_read);
61         } catch (Error e) {
62             zavai.log.error("Reading from " + device + ": " + e.message);
63             return true;
64         }
65         //stderr.printf("READ %zu chars\n", count_read);
66
67         LinuxInput.Event* ie = (LinuxInput.Event*)buf;
68         //stderr.printf("INPUT EVENT time %lu.%lu type %hu code %hu val %d\n", (ulong)ie->time.tv_sec, ie->time.tv_usec, ie->type, ie->code, ie->val);
69
70         /*
71         ts1, ts2, type, code, value = struct.unpack("LLHHI", buf)
72         #print ts1, ts2, type, code, value
73         if type == 1 and code == 119:
74             if value:
75                 #print "BUTTON RELEASE"
76                 pass
77             else:
78                 if self.last_button_press + 1 < ts1:
79                     self.last_button_press = ts1
80                     if self.button_press_handler is not None:
81                         self.button_press_handler()
82                     #print "BUTTON PRESS"
83         elif type == 5 and code == 2:
84             if value:
85                 info("Headset plugged in")
86                 self.mixer_for_headset(self)
87             else:
88                 info("Headset plugged out")
89                 self.mixer_for_handset(self)
90         */
91         return event(ie);
92     }
93
94     /// Start reading from the device
95     public override void start()
96     {
97         if (started) return;
98
99         if (fd != null)
100             close_fd();
101
102         // Open the device and listed to it using the GObject main loop
103         zavai.log.info("Opening device " + device);
104         fd = new IOChannel.file(device, "r");
105         try {
106             fd.set_encoding(null);
107         } catch (Error e) {
108             zavai.log.error("Setting encoding to null on " + device + ": " + e.message);
109         }
110         fd.set_buffered(false);
111         fd_watch = fd.add_watch(IOCondition.IN, on_input_data);
112
113         base.start();
114     }
115
116     // Stop reading from the device
117     public override void stop()
118     {
119         if (!started) return;
120
121         if (fd != null)
122         {
123             Source.remove(fd_watch);
124             close_fd();
125         }
126
127         base.stop();
128     }
129 }
130
131 public class HotKeys : zavai.Service
132 {
133     protected List<int> grabbed;
134     public signal bool hotkey(uint keycode, ulong time, bool pressed);
135
136     public HotKeys()
137     {
138         Object(name: "input.hotkeys");
139
140         grabbed = new List<int>();
141     }
142
143     // Hotkey handlink takes inspiration from
144     // http://old.nabble.com/-PATCH--Initial-non-working-implementation-of-wxWindow::%28Un%29RegisterHotKey-on-wxGTK-td14557263.html
145     private static Gdk.FilterReturn on_hotkey(Gdk.XEvent* xevent, Gdk.Event? event, void* data)
146     {
147         // Global events don't get their data translated to gdk, as there is no
148         // GdkWindow to work with, therefore we need to use the xevent, because
149         // GdkEvent* is always GDK_NOTHING
150         X.Event* xev = (X.Event*)xevent;
151
152         switch (xev->type)
153         {
154             case X.EventType.KeyPress:
155                 return zavai.input.hotkeys.my_on_hotkey(xev, true);
156             case X.EventType.KeyRelease:
157                 return zavai.input.hotkeys.my_on_hotkey(xev, false);
158             default:
159                 return Gdk.FilterReturn.CONTINUE;
160         }
161     }
162
163     //public Gdk.FilterReturn on_hotkey(Gdk.XEvent xevent, Gdk.Event? event)
164     private Gdk.FilterReturn my_on_hotkey(X.Event* xev, bool pressed)
165     {
166         // From http://tronche.com/gui/x/xlib/input/pointer-grabbing.html:
167         //
168         // A timestamp is a time value, expressed in milliseconds. It typically is the
169         // time since the last server reset. Timestamp values wrap around (after about
170         // 49.7 days). The server, given its current time is represented by timestamp
171         // T, always interprets timestamps from clients by treating half of the
172         // timestamp space as being later in time than T. One timestamp value, named
173         // CurrentTime, is never generated by the server. This value is reserved for
174         // use in requests to represent the current server time. 
175
176         if (grabbed.index((int)xev->xkey.keycode) == -1)
177             return Gdk.FilterReturn.CONTINUE;
178
179         if (hotkey(xev->xkey.keycode, xev->xkey.time, pressed))
180             return Gdk.FilterReturn.REMOVE;
181         return Gdk.FilterReturn.CONTINUE;
182     }
183
184     public void grab(int keycode, int modifiers, bool owner_events)
185     {
186         // We need to grab the keys we want to listen to
187         int res = Gdk.x11_get_default_xdisplay().grab_key(keycode, modifiers, Gdk.x11_get_default_root_xwindow(), owner_events, X.GrabMode.Async, X.GrabMode.Async);
188         if (res != 0)
189             stderr.printf("Grab result: %d\n", res); // We get BadRequest and don't know why
190         grabbed.append(keycode);
191     }
192
193     /// Start reading from the device
194     public override void start()
195     {
196         if (started) return;
197
198         //gdk_window_add_filter (NULL, _wxgtk_global_hotkey_callback, this);
199         ((Gdk.Window*)null)->add_filter((Gdk.FilterFunc)on_hotkey);
200
201         grab(160, 0, false);
202
203         base.start();
204     }
205
206     // Stop reading from the device
207     public override void stop()
208     {
209         if (!started) return;
210
211         //gdk_window_remove_filter(NULL, _wxgtk_global_hotkey_callback, this); 
212         ((Gdk.Window*)null)->remove_filter((Gdk.FilterFunc)on_hotkey);
213
214         base.stop();
215     }
216 }
217
218 public class PowerButton : zavai.Service
219 {
220     protected DevInput devinput;
221
222     public signal void power_button(Posix.timeval* time, bool pressed);
223
224     public PowerButton()
225     {
226         // FIXME: change to event0 for the power button
227         // FIXME: change to event4 for the aux button and headset button
228         string inputdev = "/dev/input/event0";
229         if (Posix.access(inputdev, Posix.R_OK) == 0)
230         {
231             zavai.log.info("Handle power button via " + inputdev);
232             // Listen via input device
233             devinput = new DevInput("input.power_button", "/dev/input/event0");
234             devinput.event += on_event;
235             devinput.request("powerbutton");
236         } else {
237             zavai.log.info("Handle power button via XGrabKey on keycode " + zavai.config.power_button_keycode.to_string());
238             // Listen via X
239             hotkeys.hotkey += on_hotkey;
240             hotkeys.grab(zavai.config.power_button_keycode, 0, false);
241             hotkeys.request("powerbutton");
242         }
243     }
244
245     protected bool on_event(LinuxInput.Event* ev)
246     {
247         if (ev->type == LinuxInput.Type.KEY && 
248             ev->code == LinuxInput.Key.POWER)
249         {
250             power_button(&(ev->time), ev->val == 0 ? false : true);
251         }
252         return true;
253     }
254
255     protected bool on_hotkey(uint keycode, ulong time, bool pressed)
256     {
257         if (keycode == zavai.config.power_button_keycode)
258         {
259             // Convert X time to a fake timeval
260             // TODO: handle wraparound
261             Posix.timeval tv = {
262                 (time_t)(time / 1000),
263                 (long)((time % 1000) * 1000)
264             };
265             power_button(&tv, pressed);
266             return true;
267         }
268         return false;
269     }
270 }
271
272
273 /*
274 # TODO:
275 #  - hook into the headset plugged/unplugged event
276 #  - if unplugged, turn on handset microphone
277 #  - if plugged, redo headest mixer settings
278 class Audio:
279     "Handle mixer settings, audio recording and headset button presses"
280     def __init__(self, button_press_handler = None):
281         self.saved_scenario = os.path.expanduser("~/.audiomap.state")
282
283         # Setup the mixer
284         # Set mixer to record from headset and handle headset button
285         self.save_scenario(self.saved_scenario)
286         self.load_scenario("/usr/share/openmoko/scenarios/voip-handset.state")
287
288         # This is a work-around because I have not found a way to query for the
289         # current headset state, I can only know when it changes. So in my
290         # system I configured oeventsd with a rule to touch this file when the
291         # headset is plugged in, and remove the file when it's plugged out.
292         if os.path.exists("/tmp/has_headset"):
293             self.mixer_for_headset(True)
294         else:
295             self.mixer_for_handset(True)
296
297         #self.mixer_set("DAPM Handset Mic", "mute")
298         #self.mixer_set("DAPM Headset Mic", "unmute")
299         #self.mixer_set("Left Mixer Sidetone Playback Sw", "unmute")
300         #self.mixer_set("ALC Mixer Mic1", "cap")
301         #self.mixer_set("Amp Spk", "mute") # We don't need the phone playing what we say
302
303         # Watch the headset button
304         self.button_press_handler = button_press_handler
305         self.input_fd = open("/dev/input/event4", "rb")
306         self.input_watch = gobject.io_add_watch(self.input_fd.fileno(), gobject.IO_IN, self.on_input_data)
307
308         self.last_button_press = 0
309         self.recorder = None
310         self.basename = None
311
312     def mixer_for_headset(self, force=False):
313         if not force and self.has_headset: return
314         info("Setting mixer for headset")
315         # TODO: find out how to disable the handset microphone: this does not
316         # seem to be sufficient
317         self.mixer_set_many(
318                 ("DAPM Handset Mic", "mute"),
319                 ("DAPM Headset Mic", "unmute"),
320                 ("Left Mixer Sidetone Playback Sw", "unmute"),
321                 ("ALC Mixer Mic1", "cap"),
322                 ("Amp Spk", "mute") # We don't need the phone playing what we say
323         )
324         self.has_headset = True
325
326     def mixer_for_handset(self, force=False):
327         if not force and not self.has_headset: return
328         info("Setting mixer for handset")
329         self.mixer_set_many(
330                 ("DAPM Handset Mic", "unmute"),
331                 ("DAPM Headset Mic", "mute"),
332                 ("Left Mixer Sidetone Playback Sw", "mute"),
333                 ("ALC Mixer Mic1", "cap"),
334                 ("Amp Spk", "mute") # We don't need the phone playing what we say
335         )
336         self.has_headset = False
337
338     def set_basename(self, basename):
339         self.basename = basename
340
341     def start_recording(self):
342         if self.basename is None:
343             raise RuntimeError("Recording requested but basename not set")
344         self.recorder = subprocess.Popen(
345             ["arecord", "-D", "hw", "-f", "cd", "-r", "8000", "-t", "wav", self.basename + ".wav"])
346
347     def start_levels(self):
348         self.recorder = subprocess.Popen(
349             ["arecord", "-D", "hw", "-f", "cd", "-r", "8000", "-t", "wav", "-V", "stereo", "/dev/null"])
350
351     def close(self):
352         if self.recorder is not None:
353             os.kill(self.recorder.pid, signal.SIGINT)
354             self.recorder.wait()
355
356         # Restore mixer settings
357         self.load_scenario(self.saved_scenario)
358
359         gobject.source_remove(self.input_watch)
360         self.input_fd.close()
361
362     def on_input_data(self, source, condition):
363         buf = self.input_fd.read(16)
364         ts1, ts2, type, code, value = struct.unpack("LLHHI", buf)
365         #print ts1, ts2, type, code, value
366         if type == 1 and code == 119:
367             if value:
368                 #print "BUTTON RELEASE"
369                 pass
370             else:
371                 if self.last_button_press + 1 < ts1:
372                     self.last_button_press = ts1
373                     if self.button_press_handler is not None:
374                         self.button_press_handler()
375                     #print "BUTTON PRESS"
376         elif type == 5 and code == 2:
377             if value:
378                 info("Headset plugged in")
379                 self.mixer_for_headset(self)
380             else:
381                 info("Headset plugged out")
382                 self.mixer_for_handset(self)
383         return True
384
385     def save_scenario(self, name):
386         res = subprocess.call(["alsactl", "store", "-f", name])
387         if res != 0:
388             raise RuntimeError("Saving audio scenario to '%s' failed" % name)
389
390     def load_scenario(self, name):
391         res = subprocess.call(["alsactl", "restore", "-f", name])
392         if res != 0:
393             raise RuntimeError("Loading audio scenario '%s' failed" % name)
394
395     def mixer_set(self, name, *args):
396         args = map(str, args)
397         res = subprocess.call(["amixer", "-q", "set", name] + args)
398         if res != 0:
399             raise RuntimeError("Setting mixer '%s' to %s failed" % (name, " ".join(args)))
400
401     # Will do this when we find out the syntax for giving amixer commands on stdin
402     def mixer_set_many(self, *args):
403         """Perform many mixer set operations via amixer --stdin"""
404         proc = subprocess.Popen(["amixer", "-q", "--stdin"], stdin=subprocess.PIPE)
405         cmd_input = []
406         for k, v in args:
407             cmd_input.append("sset " + repr(k) + " " + repr(v))
408         (out, err) = proc.communicate(input="\n".join(cmd_input))
409         res = proc.wait()
410         if res != 0:
411             raise RuntimeError("Setting mixer failed")
412 */
413
414
415 public HotKeys hotkeys = null;
416 public PowerButton power_button = null;
417
418 public void init()
419 {
420     hotkeys = new HotKeys();
421     power_button = new PowerButton();
422 }
423
424 }
425 }