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
22 [CCode (cname = "input_event", cheader_filename = "linux/input.h")]
23 public struct InputEvent
25 public Posix.timeval time;
40 import dbus.mainloop.glib
48 // For a list of dbus services, look in /etc/dbus-1/system.d/
49 public abstract class DevInput : zavai.Service
51 public string device { get; construct; }
53 protected IOChannel fd = null;
54 protected uint fd_watch = 0;
58 //usage.ResourceChanged += on_resourcechanged;
61 protected void close_fd()
67 } catch (IOChannelError e) {
68 zavai.log.error("When closing " + device + ": " + e.message);
76 public void on_resourcechanged(dynamic DBus.Object pos, string name, bool state, HashTable<string, Value?> attributes)
78 zavai.log.info("RESOURCE CHANGED " + name);
82 protected bool on_input_data(IOChannel source, IOCondition condition)
84 if (condition != IOCondition.IN) return true;
86 stderr.printf("GOT INPUT ON %s %d\n", device, source.unix_get_fd());
87 char[] buf = new char[sizeof(InputEvent)];
89 source.read_chars(buf, out count_read);
90 stderr.printf("READ %zu chars\n", count_read);
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);
95 //ssize_t count = Posix.read(source.unix_get_fd(), buf, 16);
96 //stderr.printf("READ %zd chars: %m\n", count);
99 ts1, ts2, type, code, value = struct.unpack("LLHHI", buf)
100 #print ts1, ts2, type, code, value
101 if type == 1 and code == 119:
103 #print "BUTTON RELEASE"
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:
113 info("Headset plugged in")
114 self.mixer_for_headset(self)
116 info("Headset plugged out")
117 self.mixer_for_handset(self)
122 /// Start reading from the device
123 public override void start()
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);
139 // Stop reading from the device
140 public override void stop()
142 if (!started) return;
146 Source.remove(fd_watch);
154 public class Buttons : DevInput
158 name = "input.buttons";
159 // FIXME: change to event4 for the moko
160 device = "/dev/input/event1";
166 # - hook into the headset plugged/unplugged event
167 # - if unplugged, turn on handset microphone
168 # - if plugged, redo headest mixer settings
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")
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")
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)
186 self.mixer_for_handset(True)
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
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)
199 self.last_button_press = 0
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
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
215 self.has_headset = True
217 def mixer_for_handset(self, force=False):
218 if not force and not self.has_headset: return
219 info("Setting mixer for handset")
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
227 self.has_headset = False
229 def set_basename(self, basename):
230 self.basename = basename
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"])
238 def start_levels(self):
239 self.recorder = subprocess.Popen(
240 ["arecord", "-D", "hw", "-f", "cd", "-r", "8000", "-t", "wav", "-V", "stereo", "/dev/null"])
243 if self.recorder is not None:
244 os.kill(self.recorder.pid, signal.SIGINT)
247 # Restore mixer settings
248 self.load_scenario(self.saved_scenario)
250 gobject.source_remove(self.input_watch)
251 self.input_fd.close()
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:
259 #print "BUTTON RELEASE"
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:
269 info("Headset plugged in")
270 self.mixer_for_headset(self)
272 info("Headset plugged out")
273 self.mixer_for_handset(self)
276 def save_scenario(self, name):
277 res = subprocess.call(["alsactl", "store", "-f", name])
279 raise RuntimeError("Saving audio scenario to '%s' failed" % name)
281 def load_scenario(self, name):
282 res = subprocess.call(["alsactl", "restore", "-f", name])
284 raise RuntimeError("Loading audio scenario '%s' failed" % name)
286 def mixer_set(self, name, *args):
287 args = map(str, args)
288 res = subprocess.call(["amixer", "-q", "set", name] + args)
290 raise RuntimeError("Setting mixer '%s' to %s failed" % (name, " ".join(args)))
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)
298 cmd_input.append("sset " + repr(k) + " " + repr(v))
299 (out, err) = proc.communicate(input="\n".join(cmd_input))
302 raise RuntimeError("Setting mixer failed")
305 # For a list of dbus services, look in /etc/dbus-1/system.d/
307 def __init__(self, bus = None):
309 self.bus = dbus.SystemBus()
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")
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)
325 # Request GPS resource
326 self.usage.RequestResource('GPS')
329 self.waiting_for_fix = None
332 self.usage.ReleaseResource('GPS')
335 def wait_for_fix(self, callback):
336 status = self.gps.GetFixStatus()
338 info("We already have a fix, good.")
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')
349 def on_fix_status_changed(self, status):
350 if status not in [2, 3]: return
353 self.bus.remove_signal_receiver(
354 self.on_fix_status_changed, 'FixStatusChanged', 'org.freedesktop.Gypsy.Device',
355 'org.freesmartphone.ogpsd', '/org/freedesktop/Gypsy')
357 if self.waiting_for_fix:
358 self.waiting_for_fix()
359 self.waiting_for_fix = None
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')
366 # This is taken from Zhone
368 def __init__(self, gps):
369 self.gps_ubx = gps.gps_ubx
371 self.want = set( ["NAV-STATUS", "NAV-SVINFO"] )
376 if self.busy is None:
377 pending = self.want - self.have - self.error
379 self.busy = pending.pop()
380 self.gps_ubx.SetDebugFilter(
383 reply_handler=self.on_debug_reply,
384 error_handler=self.on_debug_error,
391 def on_debug_reply(self):
392 self.have.add(self.busy)
396 def on_debug_error(self, e):
397 info(e, "error while requesting debug packet %s" % self.busy)
398 self.error.add(self.busy)
403 def __init__(self, gps):
405 self.gps_debug = GPSDebug(gps)
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()
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')
425 def on_satellites_changed(self, satellites):
427 # info("Satellite status: none")
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]
435 # self.sat_data[sat[0]][0] = sat
436 #self.print_sat_data()
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:])
448 # info("gps got ubxdebug packet", clid)
449 # info("DATA:", data)
451 def print_ubx_sat_data(self, ubxinfo):
452 info("CH ID SN ELE AZI Used Diff Alm Eph Bad Status")
454 if sv["CNO"] == 0: continue
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"]) + {
464 2: "signal acquired",
465 3: "signal unusable",
467 5: "code&carrier lock",
468 6: "code&carrier lock",
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 "---",
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",
492 """Hub that manages all the various resources that we use, and initiates
494 def __init__(self, bus = None):
496 self.waiting_for_fix = False
501 self.gps_monitor = None
507 if self.audio is not None:
510 # Close waypoints file
511 if self.gpx is not None:
514 # Stop the GPS monitor
516 self.gps_monitor.stop()
519 if self.gps is not None:
524 self.audio.start_levels()
527 self.audio = Audio(self.make_waypoint)
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()
537 self.gps_monitor = GPSMonitor(self.gps)
538 self.gps_monitor.start()
540 def start_recording(self):
542 self.gps_monitor.stop()
543 self.gps_monitor = None
549 gpstime = self.gps.gps_time.GetTime()
550 subprocess.call(["date", "-s", "@%d" % gpstime])
551 subprocess.call(["hwclock", "--systohc"])
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)
557 # Start recording the GPX track
558 self.gpx = GPX(self.basename)
559 self.gps.track_position(self.on_position_changed)
561 # Start recording in background forking arecord
562 self.audio.set_basename(self.basename)
563 self.audio.start_recording()
565 def on_position_changed(self, fields, tstamp, lat, lon, alt):
566 self.last_pos = (fields, tstamp, lat, lon, alt)
568 self.gpx.trackpoint(tstamp, lat, lon, alt)
570 def make_waypoint(self):
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))
580 class Parser(optparse.OptionParser):
581 def __init__(self, *args, **kwargs):
582 # Yes, in 2009 optparse from the *standard library* still uses old
584 optparse.OptionParser.__init__(self, *args, **kwargs)
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)
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")
598 (opts, args) = parser.parse_args()
600 if not opts.monitor and not opts.verbose:
605 dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
606 mainloop = gobject.MainLoop()
608 def on_sigint(signum, frame):
611 signal.signal(signal.SIGINT, on_sigint)
626 # Create waypoint at button press
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)
636 public Buttons buttons = null;
640 buttons = new Buttons();
642 zavai.registry.register_service(buttons);