Codesnippet: control your music player by shaking your Thinkpad around

09 February 2013

I have a lot of code snippets lying around, written as an exercise, a test or just to prove that something is possible. Once mostly functional, they just languish on my hard disk. While not the most comprehensive solutions, largely untested and sometimes just silly, they are IMHO more useful online. So I am going to try to post them more frequently in the form of short blog-posts, prefaced by “codesnippet”.

A first snippet is a Python script that uses the Lenovo Thinkpad (a T520 in my case) accelerometer as input. That means that you can tilt your laptop to issue commands, in this example controlling your music player.

To get this working, we need to install an extra kernel module and user space daemon:

sudo apt-get install tp-smapi-dkms hdapsd

The first is a dynamic kernel module exposing some extra features of the Thinkpad hardware/firmware, e.g. battery charging. It also provides a improved version of the mainline HDAPS driver. FYI, HDAPS stands for “Hard Drive Active Protection System”, which is IBM/Lenovos name for active hard-drive protection.

The second is the hdapsd disk protection user space daemon. Its official use is monitoring the acceleration values through the HDAPS interface and automatically initiating disk protection, but it also allows users to easily read out the values of the HDAPS interface.

Loading the module should show up in your dmesg output:

[  343.452582] thinkpad_ec: thinkpad_ec 0.41 loaded.
[  343.465315] tp_smapi 0.41 loading...
[  343.465395] tp_smapi successfully loaded (smapi_port=0xb2).
[  350.973926] hdaps: initial mode latch is 0x05
[  350.974084] hdaps: setting ec_rate=250, filter_order=2
[  350.974301] hdaps: device successfully initialized.
[  350.974347] input: ThinkPad HDAPS joystick emulation as /devices/virtual/input/input13
[  350.974494] input: ThinkPad HDAPS accelerometer data as /devices/virtual/input/input14
[  350.974580] hdaps: driver successfully loaded.

And you now should have a file through the sysfs interface, which contains the current orientation of the laptop in two dimensions.

cat /sys/devices/platform/hdaps/position
(471,504)

The Python script (download) below reads in those values, and uses it to provide four “inputs”: tilt left, tilt right, tilt forward, tilt backward. These are mapped to four commands for my cmus console music player, i.e. toggle play/pause, next track and volume up/down. But any console command (or Python-function) can be called.

It also does some rudementary averaging, together with threshold detection. This makes sure undeliberate changes are unlikely to trigger the command, and allows for gradual shifts of the baseline position of the laptop.

Starting up the tilt-detection/controller is a simple ./monitor.py. It prints out if/which tilt it detects, and you can comment out some other print statements for debugging.

import time, subprocess

class Orientation(object):
    def __init__(self, readfreq=1):
        self.x_init, self.y_init = self.read()
        self.x_hist = []
        self.y_hist = []

        # read in some values to smooth movement
        self.collect()

        # cooldown makes sure only a single command
        # is executed in one movement
        self.cooldown = 0

    def read(self):
        f = open('/sys/bus/platform/devices/hdaps/position')
        s = f.read().strip()
        f.close()
        x, y = s.strip('()').split(',')

        return int(x), int(y)

    def collect(self, n=10):
        # change n for smoothing over longer period
        while len(self.x_hist) < n+1:
            coords = self.read()
            self.x_hist.append(coords[0])
            self.y_hist.append(coords[1])

    @property
    def x_avg(self):
        return sum(self.x_hist)/len(self.x_hist)

    @property
    def y_avg(self):
        return sum(self.y_hist)/len(self.y_hist)

    def tick(self, timeout=0.1):
        # timeout provides the period between "ticks", i.e.
        # the frequency of reads and checks on thresholds

        self.update() # read in and process new position

        # if not in cooldown period, check if thresholds are
        # triggerd, and possibly execute a command
        if not self.cooldown:
            self.execute()
        else: # reduce cooldown periode every tick
            self.cooldown = self.cooldown - 1

        time.sleep(timeout)

    def update(self):
        self.x, self.y = self.read()

        # position relative to the moving average position
        self.x_rel, self.y_rel = o.x - o.x_avg, o.y - o.y_avg

        # keep a stack of n observations by popping the first
        # and appending to the end
        self.x_hist.pop(0)
        self.x_hist.append(self.x)
        self.y_hist.pop(0)
        self.y_hist.append(self.y)

    def tresholds(self, x_min=-30, x_max=30, y_min=-30, y_max=30):
        # check if thresholds are triggerd, turns a tuple with
        # four True/False values

        triggered = (self.x_rel < x_min, self.x_rel > x_max,
                self.y_rel < y_min, self.y_rel > y_max)

        return triggered

    def execute(self):
        t = self.tresholds()
        if t == (True, False, False, False):
            print 'Detected a forward tilt'
            subprocess.call(['cmus-remote', '-v', '-10%'])
            self.cooldown = 10

        if t == (False, True, False, False):
            print 'Detected a backward tilt'
            subprocess.call(['cmus-remote', '-v', '+10%'])
            self.cooldown = 10

        if t == (False, False, True, False):
            print 'Detected a left tilt'
            subprocess.call(['cmus-remote', '-u'])
            self.cooldown = 10

        if t == (False, False, False, True):
            print 'Detected a right tilt'
            subprocess.call(['cmus-remote', '-n'])
            self.cooldown = 10

if __name__ == '__main__':
    o = Orientation()
    while True:
        #print o.x_rel, o.y_rel, o.cooldown
        o.tick()

This entry was tagged as hardware hack python linux