2 * devinput - zavai /dev/input device handling
4 * Copyright (C) 2009 Enrico Zini <enrico@enricozini.org>
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.
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.
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
30 import dbus.mainloop.glib
38 // For a list of dbus services, look in /etc/dbus-1/system.d/
39 public abstract class DevInput : zavai.Service
41 public string device { get; construct; }
43 protected IOChannel fd = null;
44 protected uint fd_watch = 0;
48 //usage.ResourceChanged += on_resourcechanged;
51 protected void close_fd()
57 } catch (IOChannelError e) {
58 zavai.log.error("When closing " + device + ": " + e.message);
66 public void on_resourcechanged(dynamic DBus.Object pos, string name, bool state, HashTable<string, Value?> attributes)
68 zavai.log.info("RESOURCE CHANGED " + name);
72 protected bool on_input_data(IOChannel source, IOCondition condition)
74 if (condition != IOCondition.IN) return true;
76 stderr.printf("GOT INPUT ON %s %d\n", device, source.unix_get_fd());
77 char[] buf = new char[sizeof(LinuxInput.Event)];
79 source.read_chars(buf, out count_read);
80 stderr.printf("READ %zu chars\n", count_read);
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);
86 ts1, ts2, type, code, value = struct.unpack("LLHHI", buf)
87 #print ts1, ts2, type, code, value
88 if type == 1 and code == 119:
90 #print "BUTTON RELEASE"
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()
98 elif type == 5 and code == 2:
100 info("Headset plugged in")
101 self.mixer_for_headset(self)
103 info("Headset plugged out")
104 self.mixer_for_handset(self)
109 /// Start reading from the device
110 public override void start()
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);
126 // Stop reading from the device
127 public override void stop()
129 if (!started) return;
133 Source.remove(fd_watch);
141 public class Buttons : DevInput
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";
154 # - hook into the headset plugged/unplugged event
155 # - if unplugged, turn on handset microphone
156 # - if plugged, redo headest mixer settings
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")
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")
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)
174 self.mixer_for_handset(True)
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
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)
187 self.last_button_press = 0
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
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
203 self.has_headset = True
205 def mixer_for_handset(self, force=False):
206 if not force and not self.has_headset: return
207 info("Setting mixer for handset")
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
215 self.has_headset = False
217 def set_basename(self, basename):
218 self.basename = basename
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"])
226 def start_levels(self):
227 self.recorder = subprocess.Popen(
228 ["arecord", "-D", "hw", "-f", "cd", "-r", "8000", "-t", "wav", "-V", "stereo", "/dev/null"])
231 if self.recorder is not None:
232 os.kill(self.recorder.pid, signal.SIGINT)
235 # Restore mixer settings
236 self.load_scenario(self.saved_scenario)
238 gobject.source_remove(self.input_watch)
239 self.input_fd.close()
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:
247 #print "BUTTON RELEASE"
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:
257 info("Headset plugged in")
258 self.mixer_for_headset(self)
260 info("Headset plugged out")
261 self.mixer_for_handset(self)
264 def save_scenario(self, name):
265 res = subprocess.call(["alsactl", "store", "-f", name])
267 raise RuntimeError("Saving audio scenario to '%s' failed" % name)
269 def load_scenario(self, name):
270 res = subprocess.call(["alsactl", "restore", "-f", name])
272 raise RuntimeError("Loading audio scenario '%s' failed" % name)
274 def mixer_set(self, name, *args):
275 args = map(str, args)
276 res = subprocess.call(["amixer", "-q", "set", name] + args)
278 raise RuntimeError("Setting mixer '%s' to %s failed" % (name, " ".join(args)))
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)
286 cmd_input.append("sset " + repr(k) + " " + repr(v))
287 (out, err) = proc.communicate(input="\n".join(cmd_input))
290 raise RuntimeError("Setting mixer failed")
293 # For a list of dbus services, look in /etc/dbus-1/system.d/
295 def __init__(self, bus = None):
297 self.bus = dbus.SystemBus()
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")
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)
313 # Request GPS resource
314 self.usage.RequestResource('GPS')
317 self.waiting_for_fix = None
320 self.usage.ReleaseResource('GPS')
323 def wait_for_fix(self, callback):
324 status = self.gps.GetFixStatus()
326 info("We already have a fix, good.")
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')
337 def on_fix_status_changed(self, status):
338 if status not in [2, 3]: return
341 self.bus.remove_signal_receiver(
342 self.on_fix_status_changed, 'FixStatusChanged', 'org.freedesktop.Gypsy.Device',
343 'org.freesmartphone.ogpsd', '/org/freedesktop/Gypsy')
345 if self.waiting_for_fix:
346 self.waiting_for_fix()
347 self.waiting_for_fix = None
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')
354 # This is taken from Zhone
356 def __init__(self, gps):
357 self.gps_ubx = gps.gps_ubx
359 self.want = set( ["NAV-STATUS", "NAV-SVINFO"] )
364 if self.busy is None:
365 pending = self.want - self.have - self.error
367 self.busy = pending.pop()
368 self.gps_ubx.SetDebugFilter(
371 reply_handler=self.on_debug_reply,
372 error_handler=self.on_debug_error,
379 def on_debug_reply(self):
380 self.have.add(self.busy)
384 def on_debug_error(self, e):
385 info(e, "error while requesting debug packet %s" % self.busy)
386 self.error.add(self.busy)
391 def __init__(self, gps):
393 self.gps_debug = GPSDebug(gps)
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()
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')
413 def on_satellites_changed(self, satellites):
415 # info("Satellite status: none")
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]
423 # self.sat_data[sat[0]][0] = sat
424 #self.print_sat_data()
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:])
436 # info("gps got ubxdebug packet", clid)
437 # info("DATA:", data)
439 def print_ubx_sat_data(self, ubxinfo):
440 info("CH ID SN ELE AZI Used Diff Alm Eph Bad Status")
442 if sv["CNO"] == 0: continue
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"]) + {
452 2: "signal acquired",
453 3: "signal unusable",
455 5: "code&carrier lock",
456 6: "code&carrier lock",
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 "---",
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",
480 """Hub that manages all the various resources that we use, and initiates
482 def __init__(self, bus = None):
484 self.waiting_for_fix = False
489 self.gps_monitor = None
495 if self.audio is not None:
498 # Close waypoints file
499 if self.gpx is not None:
502 # Stop the GPS monitor
504 self.gps_monitor.stop()
507 if self.gps is not None:
512 self.audio.start_levels()
515 self.audio = Audio(self.make_waypoint)
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()
525 self.gps_monitor = GPSMonitor(self.gps)
526 self.gps_monitor.start()
528 def start_recording(self):
530 self.gps_monitor.stop()
531 self.gps_monitor = None
537 gpstime = self.gps.gps_time.GetTime()
538 subprocess.call(["date", "-s", "@%d" % gpstime])
539 subprocess.call(["hwclock", "--systohc"])
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)
545 # Start recording the GPX track
546 self.gpx = GPX(self.basename)
547 self.gps.track_position(self.on_position_changed)
549 # Start recording in background forking arecord
550 self.audio.set_basename(self.basename)
551 self.audio.start_recording()
553 def on_position_changed(self, fields, tstamp, lat, lon, alt):
554 self.last_pos = (fields, tstamp, lat, lon, alt)
556 self.gpx.trackpoint(tstamp, lat, lon, alt)
558 def make_waypoint(self):
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))
568 class Parser(optparse.OptionParser):
569 def __init__(self, *args, **kwargs):
570 # Yes, in 2009 optparse from the *standard library* still uses old
572 optparse.OptionParser.__init__(self, *args, **kwargs)
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)
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")
586 (opts, args) = parser.parse_args()
588 if not opts.monitor and not opts.verbose:
593 dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
594 mainloop = gobject.MainLoop()
596 def on_sigint(signum, frame):
599 signal.signal(signal.SIGINT, on_sigint)
614 # Create waypoint at button press
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)
624 public Buttons buttons = null;
628 buttons = new Buttons();
630 zavai.registry.register_service(buttons);
632 stderr.printf("ANTANI %d\n", LinuxInput.Key.POWER);