Using hdaps for screen rotation

From ThinkWiki
Jump to: navigation, search

In the middle of fighting to get the X61's screen rotate button to talk to xrandr, I realized that the laptop already has an accelerometer in it and given that, the screen rotate button was a crappy UI anyway.

So, here's a way to make the laptop automatically rotate the screen, based on the accelerometer, when in tablet mode. It was written on a system running Ubuntu 8.10, but any system with ACPI and HDAPS configured should work.

First, we need two files to tell acpid to stick a flag in the filesystem when switching between tablet and laptop modes.

  • /etc/acpi/lenovo-rotate
event=ibm/hotkey HKEY 00000080 00005009
action=/usr/bin/touch /var/lib/acpi-support/tablet
  • /etc/acpi/lenovo-rotate-back
event=ibm/hotkey HKEY 00000080 0000500a
action=/bin/rm -f /var/lib/acpi-support/tablet

Second, we need something to rotate the screen when the HDAPS device passes a certain movement threshold.

  • autorotate.py
#!/usr/bin/env python

import glob
import select
import struct
import sys
import os
from fcntl import ioctl

JS_EVENT_FMT = "IhBB"
JS_EVENT_SIZE = struct.calcsize(JS_EVENT_FMT)

# Event types --
JS_EVENT_BUTTON = 0x01 # button pressed/released
JS_EVENT_AXIS   = 0x02 # joystick moved
JS_EVENT_INIT   = 0x80 # "virtual" event flag

# ioctl magic codes for device info --
JSIOCGAXES    = 0x80016a11    # axis count
JSIOCGBUTTONS = 0x80016a12    # button count
JSIOCGNAME    = 0x81006a13    # device name

# ~0.56 is straight vertical
TILT_THRESHOLD = 0.38

# Names of xinput devices to rotate with the screen
TOUCH_DEVICES = "stylus eraser pad touch".split()

def find_hdaps():
    for filename in glob.glob("/dev/input/js*") + glob.glob("/dev/js*"):
        try: dev = open(filename, "rb")
        except (IOError, OSError): continue
        else:
            axes = struct.unpack(
                "b", ioctl(dev, JSIOCGAXES, " "))[0]
            buttons = struct.unpack(
                "b", ioctl(dev, JSIOCGBUTTONS, " "))[0]
            name = struct.unpack(
                "1024s", ioctl(dev, JSIOCGNAME, " " * 1024))[0]
            name = name.rstrip().strip("\x00")
            if name == "hdaps":
                return dev
            else:
                dev.close()

def set_state(state):
    os.system("xrandr -o %s" % state)
    pen_rot = ["normal", "right", "left", "inverted"].index(state)
    for dev in TOUCH_DEVICES:
        os.system("xsetwacom set %s rotate %d" % (dev, pen_rot))

def main(argv):
    state = "normal"

    dev = find_hdaps()
    if not dev:
        raise SystemExit("Unable to find HDAPS joystick device.")
    while select.select([dev], [], []):
        # Unpack the event and send it!
        evt = dev.read(JS_EVENT_SIZE)
        time, value, etype, number = struct.unpack(JS_EVENT_FMT, evt)
        if etype & JS_EVENT_AXIS:
            value = value / 32768.0
            if number == 0 and value > TILT_THRESHOLD:
                new_state = "normal"
            elif number == 0 and value < -TILT_THRESHOLD:
                new_state = "inverted"
            elif number == 1 and value < -TILT_THRESHOLD:
                new_state = "left"
            elif number == 1 and value > TILT_THRESHOLD:
                new_state = "right"
            else:
                new_state = state
            if state != new_state:
                if (new_state != "normal"
                    and not os.path.exists("/var/lib/acpi-support/tablet")):
                    new_state = "normal"
                if new_state != state:
                    state = new_state
                    set_state(state)

if __name__ == "__main__":
    main(sys.argv[1:])

Then, /etc/init.d/acpid reload, and run autorotate.py. Switch your laptop to tablet mode, and spin it around. The screen should rotate appropriately. If you find it rotates too easily or not easily enough, tweak the TILT_THRESHOLD variable. If you named your XInput devices for the touchscreen differently, change the TOUCH_DEVICES string.