React to power button also via X events
[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             // Listen via input device
232             devinput = new DevInput("input.power_button", "/dev/input/event0");
233             devinput.event += on_event;
234         } else {
235             // Listen via X
236             hotkeys.hotkey += on_hotkey;
237             hotkeys.grab(zavai.config.power_button_keycode, 0, false);
238             hotkeys.request("powerbutton");
239         }
240     }
241
242     protected bool on_event(LinuxInput.Event* ev)
243     {
244         if (ev->type == LinuxInput.Type.KEY && 
245             ev->code == LinuxInput.Key.POWER)
246         {
247             power_button(&(ev->time), ev->val == 0 ? false : true);
248         }
249         return true;
250     }
251
252     protected bool on_hotkey(uint keycode, ulong time, bool pressed)
253     {
254         if (keycode == zavai.config.power_button_keycode)
255         {
256             // Convert X time to a fake timeval
257             // TODO: handle wraparound
258             Posix.timeval tv = {
259                 (time_t)(time / 1000),
260                 (long)((time % 1000) * 1000)
261             };
262             power_button(&tv, pressed);
263             return true;
264         }
265         return false;
266     }
267 }
268
269
270 /*
271 # TODO:
272 #  - hook into the headset plugged/unplugged event
273 #  - if unplugged, turn on handset microphone
274 #  - if plugged, redo headest mixer settings
275 class Audio:
276     "Handle mixer settings, audio recording and headset button presses"
277     def __init__(self, button_press_handler = None):
278         self.saved_scenario = os.path.expanduser("~/.audiomap.state")
279
280         # Setup the mixer
281         # Set mixer to record from headset and handle headset button
282         self.save_scenario(self.saved_scenario)
283         self.load_scenario("/usr/share/openmoko/scenarios/voip-handset.state")
284
285         # This is a work-around because I have not found a way to query for the
286         # current headset state, I can only know when it changes. So in my
287         # system I configured oeventsd with a rule to touch this file when the
288         # headset is plugged in, and remove the file when it's plugged out.
289         if os.path.exists("/tmp/has_headset"):
290             self.mixer_for_headset(True)
291         else:
292             self.mixer_for_handset(True)
293
294         #self.mixer_set("DAPM Handset Mic", "mute")
295         #self.mixer_set("DAPM Headset Mic", "unmute")
296         #self.mixer_set("Left Mixer Sidetone Playback Sw", "unmute")
297         #self.mixer_set("ALC Mixer Mic1", "cap")
298         #self.mixer_set("Amp Spk", "mute") # We don't need the phone playing what we say
299
300         # Watch the headset button
301         self.button_press_handler = button_press_handler
302         self.input_fd = open("/dev/input/event4", "rb")
303         self.input_watch = gobject.io_add_watch(self.input_fd.fileno(), gobject.IO_IN, self.on_input_data)
304
305         self.last_button_press = 0
306         self.recorder = None
307         self.basename = None
308
309     def mixer_for_headset(self, force=False):
310         if not force and self.has_headset: return
311         info("Setting mixer for headset")
312         # TODO: find out how to disable the handset microphone: this does not
313         # seem to be sufficient
314         self.mixer_set_many(
315                 ("DAPM Handset Mic", "mute"),
316                 ("DAPM Headset Mic", "unmute"),
317                 ("Left Mixer Sidetone Playback Sw", "unmute"),
318                 ("ALC Mixer Mic1", "cap"),
319                 ("Amp Spk", "mute") # We don't need the phone playing what we say
320         )
321         self.has_headset = True
322
323     def mixer_for_handset(self, force=False):
324         if not force and not self.has_headset: return
325         info("Setting mixer for handset")
326         self.mixer_set_many(
327                 ("DAPM Handset Mic", "unmute"),
328                 ("DAPM Headset Mic", "mute"),
329                 ("Left Mixer Sidetone Playback Sw", "mute"),
330                 ("ALC Mixer Mic1", "cap"),
331                 ("Amp Spk", "mute") # We don't need the phone playing what we say
332         )
333         self.has_headset = False
334
335     def set_basename(self, basename):
336         self.basename = basename
337
338     def start_recording(self):
339         if self.basename is None:
340             raise RuntimeError("Recording requested but basename not set")
341         self.recorder = subprocess.Popen(
342             ["arecord", "-D", "hw", "-f", "cd", "-r", "8000", "-t", "wav", self.basename + ".wav"])
343
344     def start_levels(self):
345         self.recorder = subprocess.Popen(
346             ["arecord", "-D", "hw", "-f", "cd", "-r", "8000", "-t", "wav", "-V", "stereo", "/dev/null"])
347
348     def close(self):
349         if self.recorder is not None:
350             os.kill(self.recorder.pid, signal.SIGINT)
351             self.recorder.wait()
352
353         # Restore mixer settings
354         self.load_scenario(self.saved_scenario)
355
356         gobject.source_remove(self.input_watch)
357         self.input_fd.close()
358
359     def on_input_data(self, source, condition):
360         buf = self.input_fd.read(16)
361         ts1, ts2, type, code, value = struct.unpack("LLHHI", buf)
362         #print ts1, ts2, type, code, value
363         if type == 1 and code == 119:
364             if value:
365                 #print "BUTTON RELEASE"
366                 pass
367             else:
368                 if self.last_button_press + 1 < ts1:
369                     self.last_button_press = ts1
370                     if self.button_press_handler is not None:
371                         self.button_press_handler()
372                     #print "BUTTON PRESS"
373         elif type == 5 and code == 2:
374             if value:
375                 info("Headset plugged in")
376                 self.mixer_for_headset(self)
377             else:
378                 info("Headset plugged out")
379                 self.mixer_for_handset(self)
380         return True
381
382     def save_scenario(self, name):
383         res = subprocess.call(["alsactl", "store", "-f", name])
384         if res != 0:
385             raise RuntimeError("Saving audio scenario to '%s' failed" % name)
386
387     def load_scenario(self, name):
388         res = subprocess.call(["alsactl", "restore", "-f", name])
389         if res != 0:
390             raise RuntimeError("Loading audio scenario '%s' failed" % name)
391
392     def mixer_set(self, name, *args):
393         args = map(str, args)
394         res = subprocess.call(["amixer", "-q", "set", name] + args)
395         if res != 0:
396             raise RuntimeError("Setting mixer '%s' to %s failed" % (name, " ".join(args)))
397
398     # Will do this when we find out the syntax for giving amixer commands on stdin
399     def mixer_set_many(self, *args):
400         """Perform many mixer set operations via amixer --stdin"""
401         proc = subprocess.Popen(["amixer", "-q", "--stdin"], stdin=subprocess.PIPE)
402         cmd_input = []
403         for k, v in args:
404             cmd_input.append("sset " + repr(k) + " " + repr(v))
405         (out, err) = proc.communicate(input="\n".join(cmd_input))
406         res = proc.wait()
407         if res != 0:
408             raise RuntimeError("Setting mixer failed")
409 */
410
411
412 public HotKeys hotkeys = null;
413 public PowerButton power_button = null;
414
415 public void init()
416 {
417     hotkeys = new HotKeys();
418     power_button = new PowerButton();
419 }
420
421 }
422 }