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