Merge branch 'master' of http://git.hands.com/zavai
[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 abstract 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     protected void close_fd()
34     {
35         if (fd != null)
36         {
37             try {
38                 fd.shutdown(false);
39             } catch (IOChannelError e) {
40                 zavai.log.error("When closing " + device + ": " + e.message);
41             }
42
43             fd = null;
44         }
45     }
46
47     protected bool on_input_data(IOChannel source, IOCondition condition)
48     {
49         if (condition != IOCondition.IN) return true;
50
51         //stderr.printf("GOT INPUT ON %s %d\n", device, source.unix_get_fd());
52         char[] buf = new char[sizeof(LinuxInput.Event)];
53         size_t count_read;
54         try {
55             source.read_chars(buf, out count_read);
56         } catch (Error e) {
57             zavai.log.error("Reading from " + device + ": " + e.message);
58             return true;
59         }
60         //stderr.printf("READ %zu chars\n", count_read);
61
62         LinuxInput.Event* ie = (LinuxInput.Event*)buf;
63         //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);
64
65         /*
66         ts1, ts2, type, code, value = struct.unpack("LLHHI", buf)
67         #print ts1, ts2, type, code, value
68         if type == 1 and code == 119:
69             if value:
70                 #print "BUTTON RELEASE"
71                 pass
72             else:
73                 if self.last_button_press + 1 < ts1:
74                     self.last_button_press = ts1
75                     if self.button_press_handler is not None:
76                         self.button_press_handler()
77                     #print "BUTTON PRESS"
78         elif type == 5 and code == 2:
79             if value:
80                 info("Headset plugged in")
81                 self.mixer_for_headset(self)
82             else:
83                 info("Headset plugged out")
84                 self.mixer_for_handset(self)
85         */
86         return event(ie);
87     }
88
89     /// Start reading from the device
90     public override void start()
91     {
92         if (started) return;
93
94         if (fd != null)
95             close_fd();
96
97         // Open the device and listed to it using the GObject main loop
98         zavai.log.info("Opening device " + device);
99         fd = new IOChannel.file(device, "r");
100         try {
101             fd.set_encoding(null);
102         } catch (Error e) {
103             zavai.log.error("Setting encoding to null on " + device + ": " + e.message);
104         }
105         fd.set_buffered(false);
106         fd_watch = fd.add_watch(IOCondition.IN, on_input_data);
107
108         base.start();
109     }
110
111     // Stop reading from the device
112     public override void stop()
113     {
114         if (!started) return;
115
116         if (fd != null)
117         {
118             Source.remove(fd_watch);
119             close_fd();
120         }
121
122         base.stop();
123     }
124 }
125
126 public class PowerButton : DevInput
127 {
128     public signal void power_button(Posix.timeval* time, bool pressed);
129
130     public PowerButton()
131     {
132         // FIXME: change to event0 for the power button
133         // FIXME: change to event4 for the aux button and headset button
134         //device = "/dev/input/event1";
135         Object(
136         name: "input.power_button",
137         device: "/dev/input/event0"
138     );
139
140         event += on_event;
141     }
142
143     protected bool on_event(LinuxInput.Event* ev)
144     {
145         if (ev->type == LinuxInput.Type.KEY && 
146             ev->code == LinuxInput.Key.POWER)
147         {
148             power_button(&(ev->time), ev->val == 0 ? false : true);
149         }
150         return true;
151     }
152 }
153
154 /*
155 # TODO:
156 #  - hook into the headset plugged/unplugged event
157 #  - if unplugged, turn on handset microphone
158 #  - if plugged, redo headest mixer settings
159 class Audio:
160     "Handle mixer settings, audio recording and headset button presses"
161     def __init__(self, button_press_handler = None):
162         self.saved_scenario = os.path.expanduser("~/.audiomap.state")
163
164         # Setup the mixer
165         # Set mixer to record from headset and handle headset button
166         self.save_scenario(self.saved_scenario)
167         self.load_scenario("/usr/share/openmoko/scenarios/voip-handset.state")
168
169         # This is a work-around because I have not found a way to query for the
170         # current headset state, I can only know when it changes. So in my
171         # system I configured oeventsd with a rule to touch this file when the
172         # headset is plugged in, and remove the file when it's plugged out.
173         if os.path.exists("/tmp/has_headset"):
174             self.mixer_for_headset(True)
175         else:
176             self.mixer_for_handset(True)
177
178         #self.mixer_set("DAPM Handset Mic", "mute")
179         #self.mixer_set("DAPM Headset Mic", "unmute")
180         #self.mixer_set("Left Mixer Sidetone Playback Sw", "unmute")
181         #self.mixer_set("ALC Mixer Mic1", "cap")
182         #self.mixer_set("Amp Spk", "mute") # We don't need the phone playing what we say
183
184         # Watch the headset button
185         self.button_press_handler = button_press_handler
186         self.input_fd = open("/dev/input/event4", "rb")
187         self.input_watch = gobject.io_add_watch(self.input_fd.fileno(), gobject.IO_IN, self.on_input_data)
188
189         self.last_button_press = 0
190         self.recorder = None
191         self.basename = None
192
193     def mixer_for_headset(self, force=False):
194         if not force and self.has_headset: return
195         info("Setting mixer for headset")
196         # TODO: find out how to disable the handset microphone: this does not
197         # seem to be sufficient
198         self.mixer_set_many(
199                 ("DAPM Handset Mic", "mute"),
200                 ("DAPM Headset Mic", "unmute"),
201                 ("Left Mixer Sidetone Playback Sw", "unmute"),
202                 ("ALC Mixer Mic1", "cap"),
203                 ("Amp Spk", "mute") # We don't need the phone playing what we say
204         )
205         self.has_headset = True
206
207     def mixer_for_handset(self, force=False):
208         if not force and not self.has_headset: return
209         info("Setting mixer for handset")
210         self.mixer_set_many(
211                 ("DAPM Handset Mic", "unmute"),
212                 ("DAPM Headset Mic", "mute"),
213                 ("Left Mixer Sidetone Playback Sw", "mute"),
214                 ("ALC Mixer Mic1", "cap"),
215                 ("Amp Spk", "mute") # We don't need the phone playing what we say
216         )
217         self.has_headset = False
218
219     def set_basename(self, basename):
220         self.basename = basename
221
222     def start_recording(self):
223         if self.basename is None:
224             raise RuntimeError("Recording requested but basename not set")
225         self.recorder = subprocess.Popen(
226             ["arecord", "-D", "hw", "-f", "cd", "-r", "8000", "-t", "wav", self.basename + ".wav"])
227
228     def start_levels(self):
229         self.recorder = subprocess.Popen(
230             ["arecord", "-D", "hw", "-f", "cd", "-r", "8000", "-t", "wav", "-V", "stereo", "/dev/null"])
231
232     def close(self):
233         if self.recorder is not None:
234             os.kill(self.recorder.pid, signal.SIGINT)
235             self.recorder.wait()
236
237         # Restore mixer settings
238         self.load_scenario(self.saved_scenario)
239
240         gobject.source_remove(self.input_watch)
241         self.input_fd.close()
242
243     def on_input_data(self, source, condition):
244         buf = self.input_fd.read(16)
245         ts1, ts2, type, code, value = struct.unpack("LLHHI", buf)
246         #print ts1, ts2, type, code, value
247         if type == 1 and code == 119:
248             if value:
249                 #print "BUTTON RELEASE"
250                 pass
251             else:
252                 if self.last_button_press + 1 < ts1:
253                     self.last_button_press = ts1
254                     if self.button_press_handler is not None:
255                         self.button_press_handler()
256                     #print "BUTTON PRESS"
257         elif type == 5 and code == 2:
258             if value:
259                 info("Headset plugged in")
260                 self.mixer_for_headset(self)
261             else:
262                 info("Headset plugged out")
263                 self.mixer_for_handset(self)
264         return True
265
266     def save_scenario(self, name):
267         res = subprocess.call(["alsactl", "store", "-f", name])
268         if res != 0:
269             raise RuntimeError("Saving audio scenario to '%s' failed" % name)
270
271     def load_scenario(self, name):
272         res = subprocess.call(["alsactl", "restore", "-f", name])
273         if res != 0:
274             raise RuntimeError("Loading audio scenario '%s' failed" % name)
275
276     def mixer_set(self, name, *args):
277         args = map(str, args)
278         res = subprocess.call(["amixer", "-q", "set", name] + args)
279         if res != 0:
280             raise RuntimeError("Setting mixer '%s' to %s failed" % (name, " ".join(args)))
281
282     # Will do this when we find out the syntax for giving amixer commands on stdin
283     def mixer_set_many(self, *args):
284         """Perform many mixer set operations via amixer --stdin"""
285         proc = subprocess.Popen(["amixer", "-q", "--stdin"], stdin=subprocess.PIPE)
286         cmd_input = []
287         for k, v in args:
288             cmd_input.append("sset " + repr(k) + " " + repr(v))
289         (out, err) = proc.communicate(input="\n".join(cmd_input))
290         res = proc.wait()
291         if res != 0:
292             raise RuntimeError("Setting mixer failed")
293 */
294
295
296 public PowerButton power_button = null;
297
298 public void init()
299 {
300     power_button = new PowerButton();
301 }
302
303 }
304 }