2009-12-03 15:04

DBUS-reactor, or AsusOSD must die!

I love my Asus eee 701. I love it so much because I use it every day. One nice thing about it is that since it came with Linux, everything in it is well supported. Specifically, all keys do what they are supposed to do, and do it nicely.

When you click "volume up" the volume not only goes up, but you also get a graphic on screen telling you so and showing the current volume. The application that shows that is called asusosd.

However, once you turn your eee into a regular machine removing the Xandros Linux that came with it, since everything is well supported, strange things happen.

For example, when you click on "Volume Up", you get two notifications on screen: asusosd and kmix. Since kmix is much more useful than asusosd, the obvious way to fix this is removing asusosd.

But... that means other notifications are now gone. For example, "Brightness Up" does what it's supposed to do, but you get no on-screen notification (other than the screen being brighter ;-).

On other notebooks similar things happen: I have a HP Pavilion, and the "WWW" key does nothing.

So, what does a hacker do? He hacks. What do I do? I hack with python. Keep in mind, from now on, that I don't really know what heck I am doing, this is mostly a blind hack, but I think it may work as example code for the next guy stumbling into the python+dbus+hal compbination.

Here's how it works:

Whenever you press a "special key", HAL will send a DBUS message about it. You can see that using dbus-monitor. This is what happens when I press volume-down in my HP:

$ dbus-monitor --system
signal sender=org.freedesktop.DBus -> dest=:1.144 serial=2
path=/org/freedesktop/DBus; interface=org.freedesktop.DBus;
member=NameAcquired
string ":1.144"
signal sender=:1.0 -> dest=(null destination) serial=5365
path=/org/freedesktop/Hal/devices/platform_i8042_i8042_KBD_port_logicaldev_input;
interface=org.freedesktop.Hal.Device; member=Condition
string "ButtonPressed"
string "volume-down"

You can create a python program, using python-dbus, that connects to those events, and reacts in consequence (thus the dbus-reactor name, ok?)

There is a very annoying problem here, though. This is the same event in my eee:

$ dbus-monitor --system
signal sender=org.freedesktop.DBus -> dest=:1.22 serial=2
path=/org/freedesktop/DBus; interface=org.freedesktop.DBus;
member=NameAcquired
string ":1.22"
signal sender=:1.0 -> dest=(null destination) serial=632
path=/org/freedesktop/Hal/devices/computer_logicaldev_input_3;
interface=org.freedesktop.Hal.Device; member=Condition
string "ButtonPressed"
string "volume-down"

While the two are quite alike, the path parameter is different, so that can't be used, and every example I found of python-dbus usage ... uses path.

It would be nicer if my program worked in both notebooks, though. It turns out that it's not all that hard. The trick is in asking your system what the keyboard is called. To do that, you call the "FindByDeviceCapability" method of the HAL manager. Here's a snippet that does just that [listing1.py]:

import sys
import dbus

bus = dbus.SystemBus()
proxy = bus.get_object('org.freedesktop.Hal', '/org/freedesktop/Hal/Manager')
interface = dbus.Interface(proxy,dbus_interface='org.freedesktop.Hal.Manager')
print interface.FindDeviceByCapability('input.keyboard')[0]

Try it, it should work on any modern Linux system running HAL!

$ python listing1.py
/org/freedesktop/Hal/devices/platform_i8042_i8042_KBD_port_logicaldev_input

So, now, let's start with a very simple program that sets up all the DBUS stuff and connects to the 'Condition' signal [listing2.py].

# -*- coding: utf-8 -*-

import sys
import dbus
import dbus.mainloop.qt
from PyQt4 import QtCore

app=QtCore.QCoreApplication(sys.argv)

# You **must** set the event loop before getting the bus,
# or connecting to signals will not work. Funny, isn't it?

mainloop=dbus.mainloop.qt.DBusQtMainLoop(set_as_default=True)
bus = dbus.SystemBus()

hal_proxy = bus.get_object('org.freedesktop.Hal', '/org/freedesktop/Hal/Manager')
manager = dbus.Interface(hal_proxy,dbus_interface='org.freedesktop.Hal.Manager')
kbd_dev = manager.FindDeviceByCapability('input.keyboard')[0]

kbd=dbus.Interface( bus.get_object('org.freedesktop.Hal', kbd_dev),
    dbus_interface='org.freedesktop.Hal.Device')

dbus.set_default_main_loop(mainloop)

def handler(*args,**kwargs):
    print "got signal from %s" % kwargs
    print kwargs['message'].get_args_list()

def setHook():
    kbd.connect_to_signal('Condition',handler,sender_keyword='sender',
        destination_keyword='dest',
        interface_keyword='interface',
        member_keyword='member',
        path_keyword='path',
        message_keyword='message')

setHook()
print 'Entering loop'
app.exec_()

When I run it, I get this:

$ python listing2.py
Entering loop
got signal from {'sender': ':1.0', 'dest': None, 'member': 'Condition',
'interface': 'org.freedesktop.Hal.Device',
'path': dbus.ObjectPath('/org/freedesktop/Hal/devices/platform_i8042_i8042_KBD_port_logicaldev_input'),
'message': <dbus.lowlevel.SignalMessage object at 0xb75296a0>}
[dbus.String(u'ButtonPressed'), dbus.String(u'volume-down')]

So, there you go, this program is notified of the magic keys already!

Now, how can you make pretty on-screen notifications? It turns out DBUS takes care of that, too, via the session bus and org.freedesktop.Notifications interface. Here is a snippet to show a message [listing3.py]:

import sys
import dbus

# This uses the session bus because it's session-specific.
bus = dbus.SessionBus()
proxy = bus.get_object('org.freedesktop.Notifications', '/org/freedesktop/Notifications')
interface = dbus.Interface(proxy,dbus_interface='org.freedesktop.Notifications')

# This should display title and message, with no icon, both in KDE and GNOME
interface.Notify('test_runner', 0, '', 'title', 'message', [], {}, -1)

So, let's do another iteration of the main program, so that now we are notified when a magic key is pressed (luckily, regular keys cause no HAL events! ;-) [listing4.py]

import sys
import dbus
import dbus.mainloop.qt
from PyQt4 import QtCore

app=QtCore.QCoreApplication(sys.argv)
mainloop=dbus.mainloop.qt.DBusQtMainLoop(set_as_default=True)
systemBus = dbus.SystemBus()
sessionBus = dbus.SessionBus()

notify_proxy = sessionBus.get_object('org.freedesktop.Notifications', '/org/freedesktop/Notifications')
notifier = dbus.Interface(notify_proxy,dbus_interface='org.freedesktop.Notifications')

hal_proxy = systemBus.get_object('org.freedesktop.Hal', '/org/freedesktop/Hal/Manager')
manager = dbus.Interface(hal_proxy,dbus_interface='org.freedesktop.Hal.Manager')
kbd_dev = manager.FindDeviceByCapability('input.keyboard')[0]

kbd=dbus.Interface( systemBus.get_object('org.freedesktop.Hal', kbd_dev),
    dbus_interface='org.freedesktop.Hal.Device')

dbus.set_default_main_loop(mainloop)

def handler(*args,**kwargs):
    print "got signal from %s" % kwargs
    title=kwargs['message'].get_args_list()[1].replace('-',' ').title()
    notifier.Notify('System',0,'',title,'',[],{},-1)
    print kwargs['message'].get_args_list()

def setHook():
    kbd.connect_to_signal('Condition',handler,sender_keyword='sender',
        destination_keyword='dest',
        interface_keyword='interface',
        member_keyword='member',
        path_keyword='path',
        message_keyword='message')

setHook()
print 'Entering loop'
app.exec_()

And here's what happens when I press the special "Lock" key on my keyboard:

reactor1

It's trivial to make this program much better:

  • Handle multiple keyboards.
  • Adding icon support for notifications.
  • The possibility to ignore specific keys (like Volume Up/Down/Mute, which are nicely handled by KMix)
  • Support for launching a program instead of notifying.
  • React to events other than keyboard (example: plugging/unplugging of the AC power)
  • Configuration!
  • Support Qt or glib event loops

I may (or may not) do it, so if anyone wants to, it's a fun beginner's project! In less than a month, you may have a nice, useful program in your hands. And then comes fame and fortune, of course.

Comments

Comments powered by Disqus

Contents © 2000-2018 Roberto Alsina