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 stderr.printf("GOT INPUT ON %s\n", device);
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:
81 #print "BUTTON RELEASE"
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()
89 elif type == 5 and code == 2:
91 info("Headset plugged in")
92 self.mixer_for_headset(self)
94 info("Headset plugged out")
95 self.mixer_for_handset(self)
100 /// Start reading from the device
101 public override void start()
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);
115 // Stop reading from the device
116 public override void stop()
118 if (!started) return;
122 Source.remove(fd_watch);
130 public class Buttons : DevInput
134 name = "input.buttons";
135 // FIXME: change to event4 for the moko
136 device = "/dev/input/event1";
142 # - hook into the headset plugged/unplugged event
143 # - if unplugged, turn on handset microphone
144 # - if plugged, redo headest mixer settings
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")
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")
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)
162 self.mixer_for_handset(True)
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
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)
175 self.last_button_press = 0
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
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
191 self.has_headset = True
193 def mixer_for_handset(self, force=False):
194 if not force and not self.has_headset: return
195 info("Setting mixer for handset")
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
203 self.has_headset = False
205 def set_basename(self, basename):
206 self.basename = basename
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"])
214 def start_levels(self):
215 self.recorder = subprocess.Popen(
216 ["arecord", "-D", "hw", "-f", "cd", "-r", "8000", "-t", "wav", "-V", "stereo", "/dev/null"])
219 if self.recorder is not None:
220 os.kill(self.recorder.pid, signal.SIGINT)
223 # Restore mixer settings
224 self.load_scenario(self.saved_scenario)
226 gobject.source_remove(self.input_watch)
227 self.input_fd.close()
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:
235 #print "BUTTON RELEASE"
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:
245 info("Headset plugged in")
246 self.mixer_for_headset(self)
248 info("Headset plugged out")
249 self.mixer_for_handset(self)
252 def save_scenario(self, name):
253 res = subprocess.call(["alsactl", "store", "-f", name])
255 raise RuntimeError("Saving audio scenario to '%s' failed" % name)
257 def load_scenario(self, name):
258 res = subprocess.call(["alsactl", "restore", "-f", name])
260 raise RuntimeError("Loading audio scenario '%s' failed" % name)
262 def mixer_set(self, name, *args):
263 args = map(str, args)
264 res = subprocess.call(["amixer", "-q", "set", name] + args)
266 raise RuntimeError("Setting mixer '%s' to %s failed" % (name, " ".join(args)))
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)
274 cmd_input.append("sset " + repr(k) + " " + repr(v))
275 (out, err) = proc.communicate(input="\n".join(cmd_input))
278 raise RuntimeError("Setting mixer failed")
281 # For a list of dbus services, look in /etc/dbus-1/system.d/
283 def __init__(self, bus = None):
285 self.bus = dbus.SystemBus()
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")
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)
301 # Request GPS resource
302 self.usage.RequestResource('GPS')
305 self.waiting_for_fix = None
308 self.usage.ReleaseResource('GPS')
311 def wait_for_fix(self, callback):
312 status = self.gps.GetFixStatus()
314 info("We already have a fix, good.")
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')
325 def on_fix_status_changed(self, status):
326 if status not in [2, 3]: return
329 self.bus.remove_signal_receiver(
330 self.on_fix_status_changed, 'FixStatusChanged', 'org.freedesktop.Gypsy.Device',
331 'org.freesmartphone.ogpsd', '/org/freedesktop/Gypsy')
333 if self.waiting_for_fix:
334 self.waiting_for_fix()
335 self.waiting_for_fix = None
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')
342 # This is taken from Zhone
344 def __init__(self, gps):
345 self.gps_ubx = gps.gps_ubx
347 self.want = set( ["NAV-STATUS", "NAV-SVINFO"] )
352 if self.busy is None:
353 pending = self.want - self.have - self.error
355 self.busy = pending.pop()
356 self.gps_ubx.SetDebugFilter(
359 reply_handler=self.on_debug_reply,
360 error_handler=self.on_debug_error,
367 def on_debug_reply(self):
368 self.have.add(self.busy)
372 def on_debug_error(self, e):
373 info(e, "error while requesting debug packet %s" % self.busy)
374 self.error.add(self.busy)
379 def __init__(self, gps):
381 self.gps_debug = GPSDebug(gps)
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()
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')
401 def on_satellites_changed(self, satellites):
403 # info("Satellite status: none")
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]
411 # self.sat_data[sat[0]][0] = sat
412 #self.print_sat_data()
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:])
424 # info("gps got ubxdebug packet", clid)
425 # info("DATA:", data)
427 def print_ubx_sat_data(self, ubxinfo):
428 info("CH ID SN ELE AZI Used Diff Alm Eph Bad Status")
430 if sv["CNO"] == 0: continue
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"]) + {
440 2: "signal acquired",
441 3: "signal unusable",
443 5: "code&carrier lock",
444 6: "code&carrier lock",
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 "---",
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",
468 """Hub that manages all the various resources that we use, and initiates
470 def __init__(self, bus = None):
472 self.waiting_for_fix = False
477 self.gps_monitor = None
483 if self.audio is not None:
486 # Close waypoints file
487 if self.gpx is not None:
490 # Stop the GPS monitor
492 self.gps_monitor.stop()
495 if self.gps is not None:
500 self.audio.start_levels()
503 self.audio = Audio(self.make_waypoint)
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()
513 self.gps_monitor = GPSMonitor(self.gps)
514 self.gps_monitor.start()
516 def start_recording(self):
518 self.gps_monitor.stop()
519 self.gps_monitor = None
525 gpstime = self.gps.gps_time.GetTime()
526 subprocess.call(["date", "-s", "@%d" % gpstime])
527 subprocess.call(["hwclock", "--systohc"])
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)
533 # Start recording the GPX track
534 self.gpx = GPX(self.basename)
535 self.gps.track_position(self.on_position_changed)
537 # Start recording in background forking arecord
538 self.audio.set_basename(self.basename)
539 self.audio.start_recording()
541 def on_position_changed(self, fields, tstamp, lat, lon, alt):
542 self.last_pos = (fields, tstamp, lat, lon, alt)
544 self.gpx.trackpoint(tstamp, lat, lon, alt)
546 def make_waypoint(self):
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))
556 class Parser(optparse.OptionParser):
557 def __init__(self, *args, **kwargs):
558 # Yes, in 2009 optparse from the *standard library* still uses old
560 optparse.OptionParser.__init__(self, *args, **kwargs)
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)
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")
574 (opts, args) = parser.parse_args()
576 if not opts.monitor and not opts.verbose:
581 dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
582 mainloop = gobject.MainLoop()
584 def on_sigint(signum, frame):
587 signal.signal(signal.SIGINT, on_sigint)
602 # Create waypoint at button press
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)
612 public Buttons buttons = null;
616 buttons = new Buttons();
618 zavai.registry.register_service(buttons);