Access linux/input.h via a vapi file
[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 /*
22 import sys
23 import os
24 import os.path
25 import time
26 import struct
27 import signal
28 import optparse
29 import dbus
30 import dbus.mainloop.glib
31 import gobject
32 import subprocess
33 */
34
35 namespace zavai {
36 namespace input {
37
38 // For a list of dbus services, look in /etc/dbus-1/system.d/
39 public abstract class DevInput : zavai.Service
40 {
41         public string device { get; construct; }
42
43     protected IOChannel fd = null;
44     protected uint fd_watch = 0;
45
46         public DevInput()
47         {
48                 //usage.ResourceChanged += on_resourcechanged;
49         }
50
51     protected void close_fd()
52     {
53         if (fd != null)
54         {
55             try {
56                 fd.shutdown(false);
57             } catch (IOChannelError e) {
58                 zavai.log.error("When closing " + device + ": " + e.message);
59             }
60
61             fd = null;
62         }
63     }
64
65     /*
66         public void on_resourcechanged(dynamic DBus.Object pos, string name, bool state, HashTable<string, Value?> attributes)
67         {
68                 zavai.log.info("RESOURCE CHANGED " + name);
69         }
70     */
71
72     protected bool on_input_data(IOChannel source, IOCondition condition)
73     {
74                 if (condition != IOCondition.IN) return true;
75
76         stderr.printf("GOT INPUT ON %s %d\n", device, source.unix_get_fd());
77         char[] buf = new char[sizeof(LinuxInput.Event)];
78                 size_t count_read;
79                 source.read_chars(buf, out count_read);
80         stderr.printf("READ %zu chars\n", count_read);
81
82                 LinuxInput.Event* ie = (LinuxInput.Event*)buf;
83         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);
84
85         /*
86         ts1, ts2, type, code, value = struct.unpack("LLHHI", buf)
87         #print ts1, ts2, type, code, value
88         if type == 1 and code == 119:
89             if value:
90                 #print "BUTTON RELEASE"
91                 pass
92             else:
93                 if self.last_button_press + 1 < ts1:
94                     self.last_button_press = ts1
95                     if self.button_press_handler is not None:
96                         self.button_press_handler()
97                     #print "BUTTON PRESS"
98         elif type == 5 and code == 2:
99             if value:
100                 info("Headset plugged in")
101                 self.mixer_for_headset(self)
102             else:
103                 info("Headset plugged out")
104                 self.mixer_for_handset(self)
105         */
106         return true;
107     }
108
109         /// Start reading from the device
110         public override void start()
111         {
112                 if (started) return;
113
114         if (fd != null)
115             close_fd();
116
117         // Open the device and listed to it using the GObject main loop
118         fd = new IOChannel.file(device, "r");
119                 fd.set_encoding(null);
120                 fd.set_buffered(false);
121         fd_watch = fd.add_watch(IOCondition.IN, on_input_data);
122
123                 base.start();
124         }
125
126         // Stop reading from the device
127         public override void stop()
128         {
129                 if (!started) return;
130
131         if (fd != null)
132         {
133             Source.remove(fd_watch);
134             close_fd();
135         }
136
137                 base.stop();
138         }
139 }
140
141 public class Buttons : DevInput
142 {
143     public Buttons()
144     {
145         name = "input.buttons";
146         // FIXME: change to event0 for the power button
147         // FIXME: change to event4 for the aux button and headset button
148         device = "/dev/input/event1";
149     }
150 }
151
152 /*
153 # TODO:
154 #  - hook into the headset plugged/unplugged event
155 #  - if unplugged, turn on handset microphone
156 #  - if plugged, redo headest mixer settings
157 class Audio:
158     "Handle mixer settings, audio recording and headset button presses"
159     def __init__(self, button_press_handler = None):
160         self.saved_scenario = os.path.expanduser("~/.audiomap.state")
161
162         # Setup the mixer
163         # Set mixer to record from headset and handle headset button
164         self.save_scenario(self.saved_scenario)
165         self.load_scenario("/usr/share/openmoko/scenarios/voip-handset.state")
166
167         # This is a work-around because I have not found a way to query for the
168         # current headset state, I can only know when it changes. So in my
169         # system I configured oeventsd with a rule to touch this file when the
170         # headset is plugged in, and remove the file when it's plugged out.
171         if os.path.exists("/tmp/has_headset"):
172             self.mixer_for_headset(True)
173         else:
174             self.mixer_for_handset(True)
175
176         #self.mixer_set("DAPM Handset Mic", "mute")
177         #self.mixer_set("DAPM Headset Mic", "unmute")
178         #self.mixer_set("Left Mixer Sidetone Playback Sw", "unmute")
179         #self.mixer_set("ALC Mixer Mic1", "cap")
180         #self.mixer_set("Amp Spk", "mute") # We don't need the phone playing what we say
181
182         # Watch the headset button
183         self.button_press_handler = button_press_handler
184         self.input_fd = open("/dev/input/event4", "rb")
185         self.input_watch = gobject.io_add_watch(self.input_fd.fileno(), gobject.IO_IN, self.on_input_data)
186
187         self.last_button_press = 0
188         self.recorder = None
189         self.basename = None
190
191     def mixer_for_headset(self, force=False):
192         if not force and self.has_headset: return
193         info("Setting mixer for headset")
194         # TODO: find out how to disable the handset microphone: this does not
195         # seem to be sufficient
196         self.mixer_set_many(
197                 ("DAPM Handset Mic", "mute"),
198                 ("DAPM Headset Mic", "unmute"),
199                 ("Left Mixer Sidetone Playback Sw", "unmute"),
200                 ("ALC Mixer Mic1", "cap"),
201                 ("Amp Spk", "mute") # We don't need the phone playing what we say
202         )
203         self.has_headset = True
204
205     def mixer_for_handset(self, force=False):
206         if not force and not self.has_headset: return
207         info("Setting mixer for handset")
208         self.mixer_set_many(
209                 ("DAPM Handset Mic", "unmute"),
210                 ("DAPM Headset Mic", "mute"),
211                 ("Left Mixer Sidetone Playback Sw", "mute"),
212                 ("ALC Mixer Mic1", "cap"),
213                 ("Amp Spk", "mute") # We don't need the phone playing what we say
214         )
215         self.has_headset = False
216
217     def set_basename(self, basename):
218         self.basename = basename
219
220     def start_recording(self):
221         if self.basename is None:
222             raise RuntimeError("Recording requested but basename not set")
223         self.recorder = subprocess.Popen(
224             ["arecord", "-D", "hw", "-f", "cd", "-r", "8000", "-t", "wav", self.basename + ".wav"])
225
226     def start_levels(self):
227         self.recorder = subprocess.Popen(
228             ["arecord", "-D", "hw", "-f", "cd", "-r", "8000", "-t", "wav", "-V", "stereo", "/dev/null"])
229
230     def close(self):
231         if self.recorder is not None:
232             os.kill(self.recorder.pid, signal.SIGINT)
233             self.recorder.wait()
234
235         # Restore mixer settings
236         self.load_scenario(self.saved_scenario)
237
238         gobject.source_remove(self.input_watch)
239         self.input_fd.close()
240
241     def on_input_data(self, source, condition):
242         buf = self.input_fd.read(16)
243         ts1, ts2, type, code, value = struct.unpack("LLHHI", buf)
244         #print ts1, ts2, type, code, value
245         if type == 1 and code == 119:
246             if value:
247                 #print "BUTTON RELEASE"
248                 pass
249             else:
250                 if self.last_button_press + 1 < ts1:
251                     self.last_button_press = ts1
252                     if self.button_press_handler is not None:
253                         self.button_press_handler()
254                     #print "BUTTON PRESS"
255         elif type == 5 and code == 2:
256             if value:
257                 info("Headset plugged in")
258                 self.mixer_for_headset(self)
259             else:
260                 info("Headset plugged out")
261                 self.mixer_for_handset(self)
262         return True
263
264     def save_scenario(self, name):
265         res = subprocess.call(["alsactl", "store", "-f", name])
266         if res != 0:
267             raise RuntimeError("Saving audio scenario to '%s' failed" % name)
268
269     def load_scenario(self, name):
270         res = subprocess.call(["alsactl", "restore", "-f", name])
271         if res != 0:
272             raise RuntimeError("Loading audio scenario '%s' failed" % name)
273
274     def mixer_set(self, name, *args):
275         args = map(str, args)
276         res = subprocess.call(["amixer", "-q", "set", name] + args)
277         if res != 0:
278             raise RuntimeError("Setting mixer '%s' to %s failed" % (name, " ".join(args)))
279
280     # Will do this when we find out the syntax for giving amixer commands on stdin
281     def mixer_set_many(self, *args):
282         """Perform many mixer set operations via amixer --stdin"""
283         proc = subprocess.Popen(["amixer", "-q", "--stdin"], stdin=subprocess.PIPE)
284         cmd_input = []
285         for k, v in args:
286             cmd_input.append("sset " + repr(k) + " " + repr(v))
287         (out, err) = proc.communicate(input="\n".join(cmd_input))
288         res = proc.wait()
289         if res != 0:
290             raise RuntimeError("Setting mixer failed")
291
292
293 # For a list of dbus services, look in /etc/dbus-1/system.d/
294 class GPS():
295     def __init__(self, bus = None):
296         if bus is None:
297             self.bus = dbus.SystemBus()
298         else:
299             self.bus = bus
300
301         # see mdbus -s org.freesmartphone.ousaged /org/freesmartphone/Usage
302         self.usage = self.bus.get_object('org.freesmartphone.ousaged', '/org/freesmartphone/Usage')
303         self.usage = dbus.Interface(self.usage, "org.freesmartphone.Usage")
304
305         # see mdbus -s org.freesmartphone.ogpsd /org/freedesktop/Gypsy
306         gps = self.bus.get_object('org.freesmartphone.ogpsd', '/org/freedesktop/Gypsy') 
307         self.gps = dbus.Interface(gps, "org.freedesktop.Gypsy.Device")
308         self.gps_time = dbus.Interface(gps, "org.freedesktop.Gypsy.Time")
309         self.gps_position = dbus.Interface(gps, 'org.freedesktop.Gypsy.Position')
310         self.gps_ubx = dbus.Interface(gps, 'org.freesmartphone.GPS.UBX')
311         self.gps_debug = GPSDebug(self.gps_ubx)
312
313         # Request GPS resource
314         self.usage.RequestResource('GPS')
315         info("Acquired GPS")
316
317         self.waiting_for_fix = None
318
319     def close(self):
320         self.usage.ReleaseResource('GPS')
321         info("Released GPS")
322
323     def wait_for_fix(self, callback):
324         status = self.gps.GetFixStatus()
325         if status in [2, 3]:
326             info("We already have a fix, good.")
327             callback()
328             return True
329         else:
330             info("Waiting for a fix...")
331             self.waiting_for_fix = callback
332             self.bus.add_signal_receiver(
333                 self.on_fix_status_changed, 'FixStatusChanged', 'org.freedesktop.Gypsy.Device',
334                 'org.freesmartphone.ogpsd', '/org/freedesktop/Gypsy')
335             return False
336
337     def on_fix_status_changed(self, status):
338         if status not in [2, 3]: return
339
340         info("Got GPS fix")
341         self.bus.remove_signal_receiver(
342             self.on_fix_status_changed, 'FixStatusChanged', 'org.freedesktop.Gypsy.Device',
343             'org.freesmartphone.ogpsd', '/org/freedesktop/Gypsy')
344
345         if self.waiting_for_fix:
346             self.waiting_for_fix()
347             self.waiting_for_fix = None
348
349     def track_position(self, callback):
350         self.bus.add_signal_receiver(
351             callback, 'PositionChanged', 'org.freedesktop.Gypsy.Position',
352             'org.freesmartphone.ogpsd', '/org/freedesktop/Gypsy')
353
354 # This is taken from Zhone
355 class GPSDebug():
356     def __init__(self, gps):
357         self.gps_ubx = gps.gps_ubx
358         self.busy = None
359         self.want = set( ["NAV-STATUS", "NAV-SVINFO"] )
360         self.have = set()
361         self.error = set()
362
363     def _update( self ):
364         if self.busy is None:
365             pending = self.want - self.have - self.error
366             if pending:
367                 self.busy = pending.pop()
368                 self.gps_ubx.SetDebugFilter(
369                     self.busy,
370                     True,
371                     reply_handler=self.on_debug_reply,
372                     error_handler=self.on_debug_error,
373                 )
374
375     def request(self):
376         self.have = set()
377         self._update()
378
379     def on_debug_reply(self):
380         self.have.add(self.busy)
381         self.busy = None
382         self._update()
383
384     def on_debug_error(self, e):
385         info(e, "error while requesting debug packet %s" % self.busy)
386         self.error.add(self.busy)
387         self.busy = None
388         self._update()
389
390 class GPSMonitor():
391     def __init__(self, gps):
392         self.gps = gps
393         self.gps_debug = GPSDebug(gps)
394
395     def start(self):
396         # TODO: find out how come sometimes these events are not sent
397         self.gps.bus.add_signal_receiver(
398             self.on_satellites_changed, 'SatellitesChanged', 'org.freedesktop.Gypsy.Satellite',
399             'org.freesmartphone.ogpsd', '/org/freedesktop/Gypsy')
400         self.gps.bus.add_signal_receiver(
401             self.on_ubxdebug_packet, 'DebugPacket', 'org.freesmartphone.GPS.UBX',
402             'org.freesmartphone.ogpsd', '/org/freedesktop/Gypsy')
403         self.gps_debug.request()
404
405     def stop(self):
406         self.gps.bus.remove_signal_receiver(
407             self.on_satellites_changed, 'SatellitesChanged', 'org.freedesktop.Gypsy.Satellite',
408             'org.freesmartphone.ogpsd', '/org/freedesktop/Gypsy')
409         self.gps.bus.remove_signal_receiver(
410             self.on_ubxdebug_packet, 'DebugPacket', 'org.freesmartphone.GPS.UBX',
411             'org.freesmartphone.ogpsd', '/org/freedesktop/Gypsy')
412
413     def on_satellites_changed(self, satellites):
414         #if not satellites:
415         #    info("Satellite status: none")
416         #    return
417         self.gps_debug.request()
418         #info("Satellite status:")
419         #for sat in satellites:
420         #    if sat[0] not in self.sat_data:
421         #        self.sat_data[sat[0]] = [sat, None]
422         #    else:
423         #        self.sat_data[sat[0]][0] = sat
424         #self.print_sat_data()
425
426     def on_ubxdebug_packet(self, clid, length, data):
427         # In zhone it is cbUBXDebugPacket
428         #if clid == "NAV-STATUS" and data:
429         #    i = ["%s: %d" % (k, data[0][k]) for k in sorted(data[0].keys())]
430         #    info("Status:", " ".join(i))
431         ##    if data[0]['TTFF']:
432         ##        info("TTFF: %f", data[0]['TTFF']/1000.0)
433         if clid == "NAV-SVINFO":
434             self.print_ubx_sat_data(data[1:])
435         #else:
436         #    info("gps got ubxdebug packet", clid)
437         #    info("DATA:", data)
438
439     def print_ubx_sat_data(self, ubxinfo):
440         info("CH ID SN ELE AZI Used Diff Alm Eph Bad Status")
441         for sv in ubxinfo:
442             if sv["CNO"] == 0: continue
443             svid = sv["SVID"]
444             used = sv["Flags"] & 0x01
445             diff = sv["Flags"] & 0x02
446             almoreph = sv["Flags"] & 0x04
447             eph = sv["Flags"] & 0x08
448             bad = sv["Flags"] & 0x10
449             qi = ("%i: " % sv["QI"]) + {
450                 0: "idle",
451                 1: "searching",
452                 2: "signal acquired",
453                 3: "signal unusable",
454                 4: "code lock",
455                 5: "code&carrier lock",
456                 6: "code&carrier lock",
457                 7: "receiving data"
458             }[sv["QI"]]
459             info("%2d %2d %2d %3d %3d %s %s %s %s %s %s" % (
460                 sv["chn"], sv["SVID"],
461                 sv["CNO"], sv["Elev"], sv["Azim"],
462                 used and "used" or "----",
463                 diff and "diff" or "----",
464                 almoreph and "alm" or "---",
465                 eph and "eph" or "---",
466                 bad and "bad" or "---",
467                 qi))
468
469     def print_sat_data(self, satellites):
470         for sat in satellites:
471             if sat[4] == 0: continue
472             info("PRN %u" % sat[0],
473                  sat[1] and "used" or "unused",
474                  "el %u" % sat[2],
475                  "az %u" % sat[3],
476                  "snr %u" % sat[4])
477
478
479 class Hub:
480     """Hub that manages all the various resources that we use, and initiates
481     operations."""
482     def __init__(self, bus = None):
483         self.bus = bus
484         self.waiting_for_fix = False
485         self.basename = None
486         self.recorder = None
487         self.gpx = None
488         self.gps = None
489         self.gps_monitor = None
490         self.audio = None
491         self.last_pos = None
492
493     def shutdown(self):
494         # Stop recording
495         if self.audio is not None:
496             self.audio.close()
497
498         # Close waypoints file
499         if self.gpx is not None:
500             self.gpx.close()
501
502         # Stop the GPS monitor
503         if self.gps_monitor:
504             self.gps_monitor.stop()
505
506         # Release the GPS
507         if self.gps is not None:
508             self.gps.close()
509
510     def levels(self):
511         self.audio = Audio()
512         self.audio.start_levels()
513
514     def record(self):
515         self.audio = Audio(self.make_waypoint)
516         self.gps = GPS()
517         # Get a fix and start recording
518         if not self.gps.wait_for_fix(self.start_recording):
519             self.gps_monitor = GPSMonitor(self.gps)
520             self.gps_monitor.start()
521
522     def monitor(self):
523         self.audio = None
524         self.gps = GPS()
525         self.gps_monitor = GPSMonitor(self.gps)
526         self.gps_monitor.start()
527
528     def start_recording(self):
529         if self.gps_monitor:
530             self.gps_monitor.stop()
531             self.gps_monitor = None
532
533         if not self.audio:
534             return
535
536         # Sync system time
537         gpstime = self.gps.gps_time.GetTime()
538         subprocess.call(["date", "-s", "@%d" % gpstime])
539         subprocess.call(["hwclock", "--systohc"])
540
541         # Compute basename for output files
542         self.basename = time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime(gpstime))
543         self.basename = os.path.join(AUDIODIR, self.basename)
544
545         # Start recording the GPX track
546         self.gpx = GPX(self.basename)
547         self.gps.track_position(self.on_position_changed)
548
549         # Start recording in background forking arecord
550         self.audio.set_basename(self.basename)
551         self.audio.start_recording()
552
553     def on_position_changed(self, fields, tstamp, lat, lon, alt):
554         self.last_pos = (fields, tstamp, lat, lon, alt)
555         if self.gpx:
556             self.gpx.trackpoint(tstamp, lat, lon, alt)
557
558     def make_waypoint(self):
559         if self.gpx is None:
560             return
561         if self.last_pos is None:
562             self.last_pos = self.gps.gps_position.GetPosition()
563         (fields, tstamp, lat, lon, alt) = self.last_pos
564         self.gpx.waypoint(tstamp, lat, lon, alt)
565         info("Making waypoint at %s: %f, %f, %f" % (
566             time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(tstamp)), lat, lon, alt))
567
568 class Parser(optparse.OptionParser):
569     def __init__(self, *args, **kwargs):
570         # Yes, in 2009 optparse from the *standard library* still uses old
571         # style classes
572         optparse.OptionParser.__init__(self, *args, **kwargs)
573
574     def error(self, msg):
575         sys.stderr.write("%s: error: %s\n\n" % (self.get_prog_name(), msg))
576         self.print_help(sys.stderr)
577         sys.exit(2)
578
579 parser = Parser(usage="usage: %prog [options]",
580                 version="%prog "+ VERSION,
581                 description="Create a GPX and audio trackFind the times in the wav file when there is clear voice among the noise")
582 parser.add_option("-v", "--verbose", action="store_true", help="verbose mode")
583 parser.add_option("-m", "--monitor", action="store_true", help="only keep the GPS on and monitor satellite status")
584 parser.add_option("-l", "--levels", action="store_true", help="only show input levels")
585
586 (opts, args) = parser.parse_args()
587
588 if not opts.monitor and not opts.verbose:
589     def info(*args):
590         pass
591
592 # Set up dbus
593 dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
594 mainloop = gobject.MainLoop()
595
596 def on_sigint(signum, frame):
597     mainloop.quit()
598
599 signal.signal(signal.SIGINT, on_sigint)
600
601 hub = Hub()
602
603 if opts.monitor:
604     hub.monitor()
605 elif opts.levels:
606     hub.levels()
607 else:
608     hub.record()
609
610 mainloop.run()
611
612 hub.shutdown()
613
614 # Create waypoint at button press
615
616 # At button press, raise window
617 # Keep window until after 5 seconds it gets input, or until dismissed
618 # Allow to choose "do not show"
619 # Request input method when window is raised (or start a keyboard and kill it when lowered)
620
621 # Release GPS
622 */
623
624 public Buttons buttons = null;
625
626 public void init()
627 {
628         buttons = new Buttons();
629
630         zavai.registry.register_service(buttons);
631
632         stderr.printf("ANTANI %d\n", LinuxInput.Key.POWER);
633 }
634
635 }
636 }