/* * devinput - zavai /dev/input device handling * * Copyright (C) 2009 Enrico Zini * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ namespace zavai { namespace input { public class DevInput : zavai.Service { public string device { get; construct; } public signal bool event(LinuxInput.Event* ev); protected IOChannel fd = null; protected uint fd_watch = 0; public DevInput(string name, string device) { Object(name: "input.power_button", device: "/dev/input/event0"); } protected void close_fd() { if (fd != null) { try { fd.shutdown(false); } catch (IOChannelError e) { zavai.log.error("When closing " + device + ": " + e.message); } fd = null; } } protected bool on_input_data(IOChannel source, IOCondition condition) { if (condition != IOCondition.IN) return true; //stderr.printf("GOT INPUT ON %s %d\n", device, source.unix_get_fd()); char[] buf = new char[sizeof(LinuxInput.Event)]; size_t count_read; try { source.read_chars(buf, out count_read); } catch (Error e) { zavai.log.error("Reading from " + device + ": " + e.message); return true; } //stderr.printf("READ %zu chars\n", count_read); LinuxInput.Event* ie = (LinuxInput.Event*)buf; //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); /* ts1, ts2, type, code, value = struct.unpack("LLHHI", buf) #print ts1, ts2, type, code, value if type == 1 and code == 119: if value: #print "BUTTON RELEASE" pass else: if self.last_button_press + 1 < ts1: self.last_button_press = ts1 if self.button_press_handler is not None: self.button_press_handler() #print "BUTTON PRESS" elif type == 5 and code == 2: if value: info("Headset plugged in") self.mixer_for_headset(self) else: info("Headset plugged out") self.mixer_for_handset(self) */ return event(ie); } /// Start reading from the device public override void start() { if (started) return; if (fd != null) close_fd(); // Open the device and listed to it using the GObject main loop zavai.log.info("Opening device " + device); fd = new IOChannel.file(device, "r"); try { fd.set_encoding(null); } catch (Error e) { zavai.log.error("Setting encoding to null on " + device + ": " + e.message); } fd.set_buffered(false); fd_watch = fd.add_watch(IOCondition.IN, on_input_data); base.start(); } // Stop reading from the device public override void stop() { if (!started) return; if (fd != null) { Source.remove(fd_watch); close_fd(); } base.stop(); } } public class HotKeys : zavai.Service { protected List grabbed; public signal bool hotkey(uint keycode, ulong time, bool pressed); public HotKeys() { Object(name: "input.hotkeys"); grabbed = new List(); } // Hotkey handlink takes inspiration from // http://old.nabble.com/-PATCH--Initial-non-working-implementation-of-wxWindow::%28Un%29RegisterHotKey-on-wxGTK-td14557263.html private static Gdk.FilterReturn on_hotkey(Gdk.XEvent* xevent, Gdk.Event? event, void* data) { // Global events don't get their data translated to gdk, as there is no // GdkWindow to work with, therefore we need to use the xevent, because // GdkEvent* is always GDK_NOTHING X.Event* xev = (X.Event*)xevent; switch (xev->type) { case X.EventType.KeyPress: return zavai.input.hotkeys.my_on_hotkey(xev, true); case X.EventType.KeyRelease: return zavai.input.hotkeys.my_on_hotkey(xev, false); default: return Gdk.FilterReturn.CONTINUE; } } //public Gdk.FilterReturn on_hotkey(Gdk.XEvent xevent, Gdk.Event? event) private Gdk.FilterReturn my_on_hotkey(X.Event* xev, bool pressed) { // From http://tronche.com/gui/x/xlib/input/pointer-grabbing.html: // // A timestamp is a time value, expressed in milliseconds. It typically is the // time since the last server reset. Timestamp values wrap around (after about // 49.7 days). The server, given its current time is represented by timestamp // T, always interprets timestamps from clients by treating half of the // timestamp space as being later in time than T. One timestamp value, named // CurrentTime, is never generated by the server. This value is reserved for // use in requests to represent the current server time. if (grabbed.index((int)xev->xkey.keycode) == -1) return Gdk.FilterReturn.CONTINUE; if (hotkey(xev->xkey.keycode, xev->xkey.time, pressed)) return Gdk.FilterReturn.REMOVE; return Gdk.FilterReturn.CONTINUE; } public void grab(int keycode, int modifiers, bool owner_events) { // We need to grab the keys we want to listen to int res = Gdk.x11_get_default_xdisplay().grab_key(keycode, modifiers, Gdk.x11_get_default_root_xwindow(), owner_events, X.GrabMode.Async, X.GrabMode.Async); if (res != 0) stderr.printf("Grab result: %d\n", res); // We get BadRequest and don't know why grabbed.append(keycode); } /// Start reading from the device public override void start() { if (started) return; //gdk_window_add_filter (NULL, _wxgtk_global_hotkey_callback, this); ((Gdk.Window*)null)->add_filter((Gdk.FilterFunc)on_hotkey); grab(160, 0, false); base.start(); } // Stop reading from the device public override void stop() { if (!started) return; //gdk_window_remove_filter(NULL, _wxgtk_global_hotkey_callback, this); ((Gdk.Window*)null)->remove_filter((Gdk.FilterFunc)on_hotkey); base.stop(); } } public class PowerButton : zavai.Service { protected DevInput devinput; public signal void power_button(Posix.timeval* time, bool pressed); public PowerButton() { // FIXME: change to event0 for the power button // FIXME: change to event4 for the aux button and headset button string inputdev = "/dev/input/event0"; if (Posix.access(inputdev, Posix.R_OK) == 0) { zavai.log.info("Handle power button via " + inputdev); // Listen via input device devinput = new DevInput("input.power_button", "/dev/input/event0"); devinput.event += on_event; devinput.request("powerbutton"); } else { zavai.log.info("Handle power button via XGrabKey on keycode " + zavai.config.power_button_keycode.to_string()); // Listen via X hotkeys.hotkey += on_hotkey; hotkeys.grab(zavai.config.power_button_keycode, 0, false); hotkeys.request("powerbutton"); } } protected bool on_event(LinuxInput.Event* ev) { if (ev->type == LinuxInput.Type.KEY && ev->code == LinuxInput.Key.POWER) { power_button(&(ev->time), ev->val == 0 ? false : true); } return true; } protected bool on_hotkey(uint keycode, ulong time, bool pressed) { if (keycode == zavai.config.power_button_keycode) { // Convert X time to a fake timeval // TODO: handle wraparound Posix.timeval tv = { (time_t)(time / 1000), (long)((time % 1000) * 1000) }; power_button(&tv, pressed); return true; } return false; } } /* # TODO: # - hook into the headset plugged/unplugged event # - if unplugged, turn on handset microphone # - if plugged, redo headest mixer settings class Audio: "Handle mixer settings, audio recording and headset button presses" def __init__(self, button_press_handler = None): self.saved_scenario = os.path.expanduser("~/.audiomap.state") # Setup the mixer # Set mixer to record from headset and handle headset button self.save_scenario(self.saved_scenario) self.load_scenario("/usr/share/openmoko/scenarios/voip-handset.state") # This is a work-around because I have not found a way to query for the # current headset state, I can only know when it changes. So in my # system I configured oeventsd with a rule to touch this file when the # headset is plugged in, and remove the file when it's plugged out. if os.path.exists("/tmp/has_headset"): self.mixer_for_headset(True) else: self.mixer_for_handset(True) #self.mixer_set("DAPM Handset Mic", "mute") #self.mixer_set("DAPM Headset Mic", "unmute") #self.mixer_set("Left Mixer Sidetone Playback Sw", "unmute") #self.mixer_set("ALC Mixer Mic1", "cap") #self.mixer_set("Amp Spk", "mute") # We don't need the phone playing what we say # Watch the headset button self.button_press_handler = button_press_handler self.input_fd = open("/dev/input/event4", "rb") self.input_watch = gobject.io_add_watch(self.input_fd.fileno(), gobject.IO_IN, self.on_input_data) self.last_button_press = 0 self.recorder = None self.basename = None def mixer_for_headset(self, force=False): if not force and self.has_headset: return info("Setting mixer for headset") # TODO: find out how to disable the handset microphone: this does not # seem to be sufficient self.mixer_set_many( ("DAPM Handset Mic", "mute"), ("DAPM Headset Mic", "unmute"), ("Left Mixer Sidetone Playback Sw", "unmute"), ("ALC Mixer Mic1", "cap"), ("Amp Spk", "mute") # We don't need the phone playing what we say ) self.has_headset = True def mixer_for_handset(self, force=False): if not force and not self.has_headset: return info("Setting mixer for handset") self.mixer_set_many( ("DAPM Handset Mic", "unmute"), ("DAPM Headset Mic", "mute"), ("Left Mixer Sidetone Playback Sw", "mute"), ("ALC Mixer Mic1", "cap"), ("Amp Spk", "mute") # We don't need the phone playing what we say ) self.has_headset = False def set_basename(self, basename): self.basename = basename def start_recording(self): if self.basename is None: raise RuntimeError("Recording requested but basename not set") self.recorder = subprocess.Popen( ["arecord", "-D", "hw", "-f", "cd", "-r", "8000", "-t", "wav", self.basename + ".wav"]) def start_levels(self): self.recorder = subprocess.Popen( ["arecord", "-D", "hw", "-f", "cd", "-r", "8000", "-t", "wav", "-V", "stereo", "/dev/null"]) def close(self): if self.recorder is not None: os.kill(self.recorder.pid, signal.SIGINT) self.recorder.wait() # Restore mixer settings self.load_scenario(self.saved_scenario) gobject.source_remove(self.input_watch) self.input_fd.close() def on_input_data(self, source, condition): buf = self.input_fd.read(16) ts1, ts2, type, code, value = struct.unpack("LLHHI", buf) #print ts1, ts2, type, code, value if type == 1 and code == 119: if value: #print "BUTTON RELEASE" pass else: if self.last_button_press + 1 < ts1: self.last_button_press = ts1 if self.button_press_handler is not None: self.button_press_handler() #print "BUTTON PRESS" elif type == 5 and code == 2: if value: info("Headset plugged in") self.mixer_for_headset(self) else: info("Headset plugged out") self.mixer_for_handset(self) return True def save_scenario(self, name): res = subprocess.call(["alsactl", "store", "-f", name]) if res != 0: raise RuntimeError("Saving audio scenario to '%s' failed" % name) def load_scenario(self, name): res = subprocess.call(["alsactl", "restore", "-f", name]) if res != 0: raise RuntimeError("Loading audio scenario '%s' failed" % name) def mixer_set(self, name, *args): args = map(str, args) res = subprocess.call(["amixer", "-q", "set", name] + args) if res != 0: raise RuntimeError("Setting mixer '%s' to %s failed" % (name, " ".join(args))) # Will do this when we find out the syntax for giving amixer commands on stdin def mixer_set_many(self, *args): """Perform many mixer set operations via amixer --stdin""" proc = subprocess.Popen(["amixer", "-q", "--stdin"], stdin=subprocess.PIPE) cmd_input = [] for k, v in args: cmd_input.append("sset " + repr(k) + " " + repr(v)) (out, err) = proc.communicate(input="\n".join(cmd_input)) res = proc.wait() if res != 0: raise RuntimeError("Setting mixer failed") */ public HotKeys hotkeys = null; public PowerButton power_button = null; public void init() { hotkeys = new HotKeys(); power_button = new PowerButton(); } } }