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