[LINUX] Lirc and Apple Remote long press hack
#1
UPDATE 2014-04-04
---
I recently needed to rebuild my HTPC. I went for ubuntu 14.04 server as a base.
Teknos suggestion in post #13 below is far simpler to get working and seems to work well.
---


I recently set up a HTPC running Linux on an old macbook pro. I wanted to get the apple remote working with lirc and long press, because I had an apple remote lying around. I couldn't find the solution on the interweb, so I hacked one together myself. Its a python script to proxy the Lirc socket and modify the events to contain long press events (when appropriate). Its rough, but seems to work. (forgive any formatting foibles.. my first posts here)

There a few moving parts (described in the following posts).
  1. Daemon to proxy lirc port
  2. Run XBMC with custom lirc socket
  3. XBMC Lircmap.xml
  4. XBMC keymap.xml

This script is the guts of it. Save it somewhere and remember to make it executable. I have it in /usr/local/bin/lircmangled

Code:
#!/usr/bin/env python

#
# proxy the lirc socket to distinguish between short and long presses
# on apple remote buttons
#
# long press occurs when > n repeat events occur within t time of eachother.
# short press occurs when there is >= t time after an event.
#
# holding apple remote keys generates lirc events with increasing repeat counts.
# clicks on the apple remote keys can generate a few repeat lirc events, and
# if they are fast enough, can also have increasing repeat counts.
# this makes it a bit tricky to find a couple of short clicks followed by a
# long click as this looks like a continuous stream of repeat lirc events for
# the same button.  can track this by monitoring the time beteen events and
# resequencing the repeat counts where appropriate
#
# Julian Neil
#
# this software is released under the Do It Yourself License.  
# If it doesn't work for you, then fix it.

import logging
import asyncore
import asynchat
import socket
import os
import sys
import signal
import time
import threading

logging.basicConfig(level=logging.ERROR)
log = logging.getLogger(sys.argv[0])

hold_repeats = 3                            # number of repeat lirc events for long press (3 or 4 seem ok)
repeat_threshold = 0.13                     # time threshold for repeat events. (> 0.12 . might need more on a loaded machine)
proxy_socket = '/var/run/lirc/lircmangled'  # socket for mangled output (and pass-through input)
proxied_socket = '/var/run/lirc/lircd'      # lirc socket

# connect to lirc socket, mangle its output to show long presses, and forward to the proxy
class Mangler(asynchat.async_chat):

    def __init__(self, mangle_proxy, proxied_socket):
        asynchat.async_chat.__init__(self)
        self.set_terminator('\n')
        self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM)
        self.connect(proxied_socket)
        self.mangle_proxy = mangle_proxy
        self.ibuffer = ''                   # buffer for incoming lirc data
        self.last_repeats = 0               # repeat count from last lirc event
        self.last_time = 0                  # time of last lirc event
        self.last_line = None               # last lirc event split into strings
        self.hold_repeats = 0               # number of repeats the key has been held
        self.lock = threading.Lock()
        self.timer = None

    def collect_incoming_data(self, data):
        if (data):
            self.ibuffer = self.ibuffer + data

    def found_terminator(self):
        with self.lock:
            now = time.time()
            self.cancel_timer()
            bits = self.ibuffer.split(None, 5)
            # check for apple remote button presses
            if (len(bits) == 4 and 'apple' in bits[3].lower()):
                repeats = int(bits[1], 16)
                # if this is a repeat event and the time since the last press was < t we need to check for a long press
                if (repeats == self.last_repeats + 1 and now - self.last_time < repeat_threshold):
                        self.hold_repeats += 1
                        if (self.hold_repeats >= hold_repeats):
                            # if we get >= n repeats all within time t of eachother consider it a long press
                            bits[2] += '_HOLD'
                            bits[1] = "%0.2X" % (self.hold_repeats - hold_repeats)
                            # dont output repeat hold events for PLAY or MENU
                            if (self.hold_repeats == hold_repeats or ( 'PLAY' not in bits[2] and 'MENU' not in bits[2] )):
                                self.mangle_proxy.send(' '.join(bits) + '\n')
                            self.last_line = None
                        else:
                            # not enough repeats yet to distinguish between short and long press
                            self.last_line = bits
                            self.start_timer()
                else:
                    # if we have a buffered line, it must have been a short press.. so send it
                    if (self.last_line):
                        self.last_line[1]='00'
                        self.mangle_proxy.send(' '.join(self.last_line) + '\n')

                    # new button press.. dont know if it is short or long press yet
                    self.hold_repeats = 0
                    self.last_line = bits
                    self.start_timer()
                
                # remember the repeat count and time to check against when we get the next event
                self.last_repeats = repeats
                self.last_time = now
            else:
                # not an apple button press.. just forward it
                self.mangle_proxy.send(self.ibuffer + '\n')

            self.ibuffer = ''
                
    # start a timer so we know when to send a short press
    def start_timer(self):
        self.timer = threading.Timer(repeat_threshold, self.timeout)
        self.timer.start()

    def cancel_timer(self):
        if (self.timer):
            self.timer.cancel()

    # timer expired.. must have a short press.. replace its repeat count with zero
    def timeout(self):
        with self.lock:
            if (self.last_line and time.time() - self.last_time > repeat_threshold):
                self.last_line[1]='00'
                self.mangle_proxy.send(' '.join(self.last_line) + '\n')
                self.last_line = None

class MangleProxy(asyncore.dispatcher_with_send):

    handler_id = 1

    def __init__(self, socket, proxied_socket):
        asyncore.dispatcher_with_send.__init__(self, socket)
        self.handler_id = MangleProxy.handler_id
        MangleProxy.handler_id += 1
        log.info("Creating handler %d", self.handler_id)
        self.mangler = Mangler(self, proxied_socket)

    def handle_read(self):
        data = self.recv(8192)
        if data:
            self.mangler.push(data)

    def handle_close(self):
        log.info("Closing handler %d", self.handler_id)
        self.mangler.close();
        self.close();

# listen for and accept connects to the proxy socket
class MangleServer(asyncore.dispatcher):

    def __init__(self, proxy_socket, proxied_socket):
        asyncore.dispatcher.__init__(self)
        log.info('Starting server on %s', proxy_socket)
        self.proxy_socket = proxy_socket
        self.proxied_socket = proxied_socket

        self.remove_proxy_socket()

        self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM)
        self.set_reuse_addr()
        self.bind(self.proxy_socket)
        self.listen(5)

        os.chmod(self.proxy_socket, 0666)
        self.closed = False

    def remove_proxy_socket(self):
        try:
            if (os.path.exists(self.proxy_socket)):
                os.unlink(self.proxy_socket)
        except OSError, error:
            log.error('Unable to remove socket file %s : %s', self.proxy_socket, str(error))
            raise

    def handle_accept(self):
        pair = self.accept()
        if pair is None:
            pass
        else:
            sock, addr = pair
            log.info('Incoming connection from %s', repr(addr))
            handler = MangleProxy(sock, self.proxied_socket)

    def handle_close(self):
        if (not self.closed):
            log.info('Stopping server on %s', self.proxy_socket)
            self.close()
            self.remove_proxy_socket()
            self.closed = True

    # fix for asyncore bug causing 100% cpu when listening on unix domain sockets
    def writable(self):
        return False

server = None

try:
    # ignore HUPs.. like a good daemon
    signal.signal(signal.SIGHUP, lambda x,y: None)

    server = MangleServer(proxy_socket, proxied_socket)
    asyncore.loop()

finally:
    if (server):
        server.handle_close()

You will need some sort of init script to start it at boot time. I'm using ubuntu 10.04, so here is a simple upstart script. I have it saved as /etc/init/lircmangled.conf

Code:
description    "Lirc Mangle Daemon"

start on filesystem
stop on runlevel [!2345]

respawn
respawn limit 10 5

pre-start script
    mkdir -p -m0755 /var/run/lirc
end script

exec /usr/local/bin/lircmangled
Reply
#2
You'll need a custom Lircmap.xml and keymap.xml. I use the follwing. You might want to change the keymap if you want different functions mapped to the remote buttons.

~/.xbmc/userdata/Lircmap.xml
Code:
<lircmap>
    <remote device="Apple_A1156">
        <obc1>VOLUP</obc1>
        <obc2>VOLDOWN</obc2>
        <obc3>BACKWARD</obc3>
        <obc4>FORWARD</obc4>
        <obc5>PLAY</obc5>
        <obc6>MENU</obc6>
        <obc7>PLAY_HOLD</obc7>
        <obc8>MENU_HOLD</obc8>
        <obc9>BACKWARD_HOLD</obc9>
        <obc10>FORWARD_HOLD</obc10>
        <obc11>VOLUP_HOLD</obc11>
        <obc12>VOLDOWN_HOLD</obc12>
    </remote>
</lircmap>

~/.xbmc/userdata/keymaps/keymap.xml
Code:
<keymap>
  <global>
    <universalremote>
      <!-- plus       -->      <obc1>Up</obc1>
      <!-- minus      -->      <obc2>Down</obc2>
      <!-- left       -->      <obc3>Left</obc3>
      <!-- right      -->      <obc4>Right</obc4>
      <!-- center     -->      <obc5>Select</obc5>
      <!-- menu       -->      <obc6>PreviousMenu</obc6>
      <!-- hold center  -->    <obc7>Fullscreen</obc7>
      <!-- hold menu  -->      <obc8>ContextMenu</obc8>
      <!-- hold left  -->      <obc9>Left</obc9>
      <!-- hold right -->      <obc10>Right</obc10>
      <!-- hold up    -->      <obc11>Up</obc11>
      <!-- hold down  -->      <obc12>Down</obc12>
    </universalremote>
  </global>
  <Home>
    <universalremote>
      <obc6>XBMC.ActivateWindow(Favourites)</obc6>
      <obc8>ActivateWindow(shutdownmenu)</obc8>
    </universalremote>
  </Home>
  <MyFiles>
    <universalremote>
    </universalremote>
  </MyFiles>
  <MyMusicPlaylist>
    <universalremote>
      <obc6>Playlist</obc6>
    </universalremote>
  </MyMusicPlaylist>
  <MyMusicPlaylistEditor>
    <universalremote>
      <obc6>ParentDir</obc6>
    </universalremote>
  </MyMusicPlaylistEditor>
  <MyMusicFiles>
    <universalremote>
      <obc6>ParentDir</obc6>
    </universalremote>
  </MyMusicFiles>
  <MyMusicLibrary>
    <universalremote>
      <obc6>ParentDir</obc6>
    </universalremote>
  </MyMusicLibrary>
  <FullscreenVideo>
    <universalremote>
      <obc1>VolumeUp</obc1>
      <obc2>VolumeDown</obc2>
      <obc3>StepBack</obc3>
      <obc4>StepForward</obc4>
      <obc5>Pause</obc5>
      <obc6>OSD</obc6>
      <obc7>Stop</obc7>
      <obc8>Fullscreen</obc8>
      <obc9>StepBack</obc9>
      <obc10>StepForward</obc10>
      <obc11>VolumeUp</obc11>
      <obc12>VolumeDown</obc12>
    </universalremote>
  </FullscreenVideo>
  <VideoTimeSeek>
    <universalremote>
      <obc5>Select</obc5>
      <obc7>Select</obc7>
    </universalremote>
  </VideoTimeSeek>
  <FullscreenInfo>
    <universalremote>
      <obc6>Close</obc6>
    </universalremote>
  </FullscreenInfo>
  <PlayerControls>
    <universalremote>
      <obc6>Close</obc6>
    </universalremote>
  </PlayerControls>
  <Visualisation>
    <universalremote>
      <obc1>VolumeUp</obc1>
      <obc2>VolumeDown</obc2>
      <obc3>SkipPrevious</obc3>
      <obc4>SkipNext</obc4>
      <obc5>Pause</obc5>
      <obc6>Fullscreen</obc6>
      <obc7>XBMC.ActivateWindow(MusicOSD)</obc7>
      <obc8>Stop</obc8>
    </universalremote>
  </Visualisation>
  <MusicOSD>
    <universalremote>
      <obc6>Close</obc6>
    </universalremote>
  </MusicOSD>
  <VisualisationSettings>
    <universalremote>
      <obc6>Close</obc6>
    </universalremote>
  </VisualisationSettings>
  <VisualisationPresetList>
    <universalremote>
      <obc6>Close</obc6>
    </universalremote>
  </VisualisationPresetList>
  <SlideShow>
    <universalremote>
      <obc1>ZoomIn</obc1>
      <obc2>ZoomOut</obc2>
      <obc3>PreviousPicture</obc3>
      <obc4>NextPicture</obc4>
      <obc6>Stop</obc6>
      <obc7>Info</obc7>
      <obc8>Rotate</obc8>
      <obc11>ZoomIn</obc11>
      <obc12>ZoomOut</obc12>
    </universalremote>
  </SlideShow>
  <ScreenCalibration>
    <universalremote>
      <obc5>NextCalibration</obc5>
    </universalremote>
  </ScreenCalibration>
  <GUICalibration>
    <universalremote>
      <obc5>NextCalibration</obc5>
    </universalremote>
  </GUICalibration>
  <SelectDialog>
    <universalremote>
      <obc6>Close</obc6>
    </universalremote>
  </SelectDialog>
  <VideoOSD>
    <universalremote>
      <obc6>Close</obc6>
    </universalremote>
  </VideoOSD>
  <VideoMenu>
    <universalremote>
      <obc5>Select</obc5>
      <obc6>Stop</obc6>
      <obc7>OSD</obc7>
      <obc8></obc8>
    </universalremote>
  </VideoMenu>
  <OSDVideoSettings>
    <universalremote>
      <obc6>Close</obc6>
    </universalremote>
  </OSDVideoSettings>
  <OSDAudioSettings>
    <universalremote>
      <obc6>Close</obc6>
    </universalremote>
  </OSDAudioSettings>
  <VideoBookmarks>
    <universalremote>
      <obc6>Close</obc6>
    </universalremote>
  </VideoBookmarks>
  <MyVideoLibrary>
    <universalremote>
      <obc6>ParentDir</obc6>
      <obc7>Info</obc7>
    </universalremote>
  </MyVideoLibrary>
  <MyVideoFiles>
    <universalremote>
      <obc6>ParentDir</obc6>
      <obc7>Info</obc7>
    </universalremote>
  </MyVideoFiles>
  <MyVideoPlaylist>
    <universalremote>
      <obc6>Playlist</obc6>
    </universalremote>
  </MyVideoPlaylist>
  <VirtualKeyboard>
    <universalremote>
      <obc6>Close</obc6>
    </universalremote>
  </VirtualKeyboard>
  <ContextMenu>
    <universalremote>
      <obc6>Close</obc6>
    </universalremote>
  </ContextMenu>
  <FileStackingDialog>
    <universalremote>
      <obc6>Close</obc6>
    </universalremote>
  </FileStackingDialog>
  <Scripts>
    <universalremote>
    </universalremote>
  </Scripts>
  <NumericInput>
    <universalremote>
      <obc6>Close</obc6>
    </universalremote>
  </NumericInput>
  <Weather>
    <universalremote>
      <obc6>PreviousMenu</obc6>
    </universalremote>
  </Weather>
  <Settings>
    <universalremote>
      <obc6>PreviousMenu</obc6>
    </universalremote>
  </Settings>
  <AddonBrowser>
    <universalremote>
      <obc6>ParentDir</obc6>
    </universalremote>
  </AddonBrowser>
  <AddonInformation>
    <universalremote>
      <obc6>Close</obc6>
    </universalremote>
  </AddonInformation>
  <AddonSettings>
    <universalremote>
      <obc6>Close</obc6>
    </universalremote>
  </AddonSettings>
  <TextViewer>
    <universalremote>
      <obc6>Close</obc6>
    </universalremote>
  </TextViewer>
  <MyPictures>
    <universalremote>
      <obc6>ParentDir</obc6>
    </universalremote>
  </MyPictures>
  <MyPicturesSettings>
    <universalremote>
      <obc6>PreviousMenu</obc6>
    </universalremote>
  </MyPicturesSettings>
  <MyWeatherSettings>
    <universalremote>
      <obc6>PreviousMenu</obc6>
    </universalremote>
  </MyWeatherSettings>
  <MyMusicSettings>
    <universalremote>
      <obc6>PreviousMenu</obc6>
    </universalremote>
  </MyMusicSettings>
  <SystemSettings>
    <universalremote>
      <obc6>PreviousMenu</obc6>
    </universalremote>
  </SystemSettings>
  <MyVideoSettings>
    <universalremote>
      <obc6>PreviousMenu</obc6>
    </universalremote>
  </MyVideoSettings>
  <NetworkSettings>
    <universalremote>
      <obc6>PreviousMenu</obc6>
    </universalremote>
  </NetworkSettings>
  <AppearanceSettings>
    <universalremote>
      <obc6>PreviousMenu</obc6>
    </universalremote>
  </AppearanceSettings>
  <Profiles>
    <universalremote>
      <obc6>PreviousMenu</obc6>
    </universalremote>
  </Profiles>
  <systeminfo>
    <universalremote>
      <obc6>PreviousMenu</obc6>
    </universalremote>
  </systeminfo>
  <shutdownmenu>
    <universalremote>
      <obc6>PreviousMenu</obc6>
    </universalremote>
  </shutdownmenu>
  <submenu>
    <universalremote>
      <obc6>PreviousMenu</obc6>
    </universalremote>
  </submenu>
  <MusicInformation>
    <universalremote>
      <obc6>Close</obc6>
    </universalremote>
  </MusicInformation>
  <MovieInformation>
    <universalremote>
      <obc6>Close</obc6>
    </universalremote>
  </MovieInformation>
  <LockSettings>
    <universalremote>
      <obc6>Close</obc6>
    </universalremote>
  </LockSettings>
  <ProfileSettings>
    <universalremote>
      <obc6>Close</obc6>
    </universalremote>
  </ProfileSettings>
  <PictureInfo>
    <universalremote>
      <obc3>Left</obc3>
      <obc4>Right</obc4>
      <obc6>Close</obc6>
    </universalremote>
  </PictureInfo>
  <Teletext>
    <universalremote>
      <obc6>Close</obc6>
    </universalremote>
  </Teletext>
  <Favourites>
    <universalremote>
      <obc6>Close</obc6>
    </universalremote>
  </Favourites>
  <FileBrowser>
    <universalremote>
      <obc6>Close</obc6>
    </universalremote>
  </FileBrowser>
</keymap>


Finally, when you start XBMC you need to tell it to use the new lirc socket using the -l option.. e.g.
Code:
xbmc --standalone -l /var/run/lirc/lircmangled
Reply
#3
Thanks a lot for posting this, Julian! Works quite nicely for me as well. Although I only have it running for 5 minutes by now, it seems do it's job without any big issues. Only thing I noticed is that keeping a button pressed for a prolonged amount of time may sometimes still result in additional events. (Didn't have time to look into it yet though.)

Btw, shouldn't this be included in Lirc itself?

Anyway, thanks again for posting!

Cheers, Remco
Reply
#4
This should work with the new alu remote?
Reply
#5
Nice job! Thank you.

Just I had to change
proxied_socket = '/dev/lircd'
This is default location in debian.
Reply
#6
Glad it was of use to someone else Smile

It should work fine for aluminium remote.. so long as the events reported by lirc have APPLE.... in them. Of course if they dont, you could always edit the lirc config (or my script).

If the aluminium remote has more buttons, you might want to customise the xbmc lircmap and keymap as well.

Jules
Reply
#7
Thank you! this is exactly what I was looking for. I will try your work.
Reply
#8
I would love to get the long key presses working... but they aren't...

I'm using the white Apple A1156, it works, but no holding of keys....

when i use your script, it doesn't map things right...

I think this is maybe because my /etc/lirc/lircd.conf file is mapping different names? and those names have to match Lircmap.xmlHuh

If that's the case, maybe posting your /etc/lirc/lircd.conf file would be helpful, because I used "irrecord" to create mine, and I'm not sure how to add a HOLD button key in there.

Reply
#9
(2012-03-13, 16:16)Kissell Wrote: I think this is maybe because my /etc/lirc/lircd.conf file is mapping different names? and those names have to match Lircmap.xmlHuh

If that's the case, maybe posting your /etc/lirc/lircd.conf file would be helpful, because I used "irrecord" to create mine, and I'm not sure how to add a HOLD button key in there.

Hi Kissell.

It isn't possible to configure lircd.conf to recognise long presses, or I would have done that instead of resorting to this workaround. This script runs in the background, taking the output from lirc and post-processing it to identify long presses. It then posts output containing long presses (using a format identical to lirc) on a different socket. xbmc then reads this socket instead of the usual lirc socket.

I am using the lircd config for apple remote that comes with the ubuntu lirc package. It utilizes the macmini driver.
It is located at /usr/share/lirc/remotes/apple/lircd.conf.macmini .. included here, but I'm not sure that it will help.

Code:
#
# this config file was automatically generated
# using lirc-0.8.2(macmini) on Tue Dec 11 11:35:26 2007
#
# contributed by Sebastian Schaetzel
#
# brand:                       Apple
# model no. of remote control: A1156
# devices being controlled by this remote: Mac mini, MacBookPro 15"
# SantaRosa (3.1), MacBook2
#

begin remote

  name  Apple_A1156
  bits            8
  eps            30
  aeps          100

  one             0     0
  zero            0     0
  pre_data_bits   24
  pre_data       0x87EE81
  gap          211982
  toggle_bit_mask 0x0
  ignore_mask 0x0000ff01

      begin codes
          VOLUP                    0x0B
          VOLDOWN                  0x0D
          BACKWARD                 0x08
          FORWARD                  0x07
          PLAY                     0x04
          MENU                     0x02
      end codes

end remote

Reply
#10
has anyone tried to get this script to work under openelec?
Reply
#11
Yes, I have gotten this working in openelec. Although it is somewhat difficult. I had to compile my own openelec build because lircd was remove from the official build way back. (I haven't been tinkering with openelec in about 6 months so this may have changed with newer builds)

I am not an expert linux guy and figured out how to get this working after many many hours. I wanted to make a write up but put it off too long and not I forgot how I got this working. I will try, over the next few weeks to go back through the process and make a writeup. The Apple Remote w/ long presses really is a nice remote.
Reply
#12
Looks like the new official build of Openelec has the "lirc_serial" module included so you don't have to compile your own kernel.

I just created a writeup on how I got this working on openelec. Hope this helps...

http://openelec.tv/forum/103-infared-rem...sses#48208
Reply
#13
Cough, cough!
Way easier, and seeing this thread inspired me to prove it. Long keypress support too!
See
http://teknogeekz.com/blog/?p=422
And try not to drive up the cost of the Apple IR replacent receivers!
Reply
#14
Nice.. I had a go at getting the atvclient to compile a while back, but had no luck. Will give it a go. Thanks tekno.
Reply
#15
Forgive me if i'm wrong. But, I don't believe this will work on openelec since it's a "Read-Only" operating system and doesn't have your apt-get. Any extra modules would have to be compiled into the OS.
Reply

Logout Mark Read Team Forum Stats Members Help
[LINUX] Lirc and Apple Remote long press hack1