OYB software, raspberrypi

Control your Raspberry Pi with your TV remote


This little script will allow you to control your Raspberry Pi using the buttons in your regular TV remote.


You can press the following buttons on the remote.

  • OK       – launch kodi
  • up       – launch browser
  • down  – change wallpaper (this is a custom script of mine)
  • left      – suspend your desktop computer
  • back    – halt Raspberry Pi
  • stop     – enter “mouse mode”, to move the mouse with the arrows. OK to double click
  • play     – exit “mouse mode”

These bindings are set as an example. Each person should change them to suit their own needs.


First, follow the instructions to compile and install libcec. More details below.

Then, get the code from my gist, and create an executable script

wget https://gist.githubusercontent.com/nachoparker/96fa8036d93603b3eee01ee69d3c1606/raw/74ae036ae3e50e7153231e0694c2ca7dd8f43c8a/picec.sh
chmod +x picec.sh

You can run picec.sh  from whatever “startup programs” configuration your desktop provides. In my case, I just add the following line at the end of /etc/rc.local


Optionally, you can install xdotool  for moving the mouse, and osd_cat  for showing information in a TV OSD fashion. More details below.

sudo apt-get install xosd-bin xdotool

As a means to control our RPi, we will use the CEC (Consumer Electronics Control) feature of HDMI, which is a standard protocol to send commands back and forth through HDMI.

CEC allows consumer devices to control each other with or without user intervention. The GPU of the Raspberry Pi already supports the CEC protocol, as most modern TVs do. There is a compatibility list here.

An often missed requirement is that your HDMI cable must support CEC as well. Most cables do, but some of the cheapest ones cannot talk CEC, so be careful with this.

In order to speak to our TV using the CEC standard, we make use of the libcec library by Pulse Eight. This library is already included in Kodi out of the box, and it is the reason we can readily control our media center with our remote.

From their website, we can follow the instructions to compile and install libcec on the Raspberry Pi.

sudo apt-get update
sudo apt-get install cmake libudev-dev libxrandr-dev python-dev swig
git clone https://github.com/Pulse-Eight/platform.git
mkdir platform/build
cd platform/build
cmake ..
sudo make install
git clone https://github.com/Pulse-Eight/libcec.git
mkdir libcec/build
cd libcec/build
cmake -DRPI_INCLUDE_DIR=/opt/vc/include -DRPI_LIB_DIR=/opt/vc/lib ..
make -j4
sudo make install
sudo ldconfig

After this, we obtain the library, some python bindings and a wrapper utility in C called cec-client .

We can now list connected devices,

tv# cec-client -l
libCEC version: 3.1.0, git revision: libcec-3.1.0+5-6d68d21, compiled on Fri  8 Apr 15:44:58 UTC 2016 by nacho@tv on Linux 4.1.19-v7+ (armv7l), features: P8_USB, DRM, P8_detect, randr, RPi
Found devices: 1

device:              1
com port:            RPI
vendor id:           2708
product id:          1001
firmware version:    1
type:                Raspberry Pi

, turn the TV on and off from SSH,

echo "on 0"      | cec-client -s # turn on the television
echo "standby 0" | cec-client -s # turn it off

, and even configure the TV. To see all available commands:

tv# echo h | cec-client -s -d 1
opening a connection to the CEC adapter...

Available commands:

[tx] {bytes}              transfer bytes over the CEC line.
[txn] {bytes}             transfer bytes but don't wait for transmission ACK.
[on] {address}            power on the device with the given logical address.
[standby] {address}       put the device with the given address in standby mode.
[la] {logical address}    change the logical address of the CEC adapter.
[p] {device} {port}       change the HDMI port number of the CEC adapter.
[pa] {physical address}   change the physical address of the CEC adapter.
[as]                      make the CEC adapter the active source.
[is]                      mark the CEC adapter as inactive source.
[osd] {addr} {string}     set OSD message on the specified device.
[ver] {addr}              get the CEC version of the specified device.
[ven] {addr}              get the vendor ID of the specified device.
[lang] {addr}             get the menu language of the specified device.
[pow] {addr}              get the power status of the specified device.
[name] {addr}             get the OSD name of the specified device.
[poll] {addr}             poll the specified device.
[lad]                     lists active devices on the bus
[ad] {addr}               checks whether the specified device is active.
[at] {type}               checks whether the specified device type is active.
[sp] {addr}               makes the specified physical address active.
[spl] {addr}              makes the specified logical address active.
[volup]                   send a volume up command to the amp if present
[voldown]                 send a volume down command to the amp if present
[mute]                    send a mute/unmute command to the amp if present
[self]                    show the list of addresses controlled by libCEC
[scan]                    scan the CEC bus and display device info
[mon] {1|0}               enable or disable CEC bus monitoring.
[log] {1 - 31}            change the log level. see cectypes.h for values.
[ping]                    send a ping command to the CEC adapter.
[bl]                      to let the adapter enter the bootloader, to upgrade
                          the flash rom.
[r]                       reconnect to the CEC adapter.
[h] or [help]             show this help.
[q] or [quit]             to quit the CEC test client and switch off all
                          connected CEC devices.

As a quick and dirty way of playing with this, I wrote this little script that just parses the output of the cec-client utility. I figured it would be faster to implement than using the C library and end up calling system()  or popen() anyway.

There is lots of room for improvement, but I hope this can give you ideas to get started.


# Simple script to control your Raspberry Pi with your TV remote using libcec
# Copyleft 2017 by Ignacio Nunez Hernanz <nacho _a_t_ ownyourbits _d_o_t_ com>
# GPL licensed (see end of file) * Use at your own risk!
# You can set this to be run in your desktop "startup programs", or 
# at the last line of /etc/rc.local, just to say a couple options
# keys:
#   OK   - launch kodi 
#   up   - launch browser
#   down - change wallpaper (this is a custom script of mine)
#   left - suspend my desktop computer
#   back - exit desktop session
# You can hit 'stop' to enter "mouse mode", or 'play' to exit it.
# In "mouse mode" you can move around the mouse with the remote arrows, hit ok
# to double-click.
# This is just an example to provide ideas, so you should change what each button does

USER=pi                        # user that will be logged in
OSDCOLOR=red                   # color for xosd messages
MOUSESPEED=50                  # how many pixels a mouse will move on each press
#dbg=echo                      # uncomment to print the command without running it
VERBOSE=0                      # print echov lines
#DRY_RUN="yes"                 # uncomment to actually ignore button presses

type cec-client &>/dev/null || { echo "cec-client is requiered"; exit; }
type xdotool    &>/dev/null || NOXDOTOOL=#

USR_CMD="su - $USER -c"

         d(){ eval "$NOXDOTOOL $@"                                            ; }
     echov(){ [[ "$VERBOSE" == "1" ]] && echo $@                              ; }
filter_key(){ grep -q "key pressed: $1 .* duration" <( echo "$2" )            ; }
mouse_move(){ XAUTHORITY="$_XAUTH" DISPLAY=:0 xdotool mousemove_relative -- $@; }
mouseclick(){ XAUTHORITY="$_XAUTH" DISPLAY=:0 xdotool click --repeat 2 1      ; }
   osdecho(){ type osd_cat &>/dev/null && echo "$@" | \
              XAUTHORITY="$_XAUTH" DISPLAY=:0 osd_cat -ptop -Acenter -c$OSDCOLOR; }

while :; do 
  cec-client | while read l; do
    echov $l

    [[ "$DRY_RUN" != "" ]] && continue

     pgrep kodi && { echov "Ignoring key, because Kodi is running"; continue; }

    if  filter_key "select" "$l"; then
      [[ "$MOUSEMODE" == "1" ]] && mouseclick || {
        $dbg $USR_CMD kodi
        killall cec-client
    if  filter_key "up" "$l"; then
      [[ "$MOUSEMODE" == "1" ]] && mouse_move 0 -$MOUSESPEED || \
      $dbg $USR_CMD "DISPLAY=\":0\" x-www-browser" &
    if  filter_key "left" "$l"; then
      [[ "$MOUSEMODE" == "1" ]] && mouse_move -$MOUSESPEED 0 || \
        $dbg nohup su - nacho -c "ssh nacho@desktop sudo pm-suspend" &
    if  filter_key "down" "$l"; then
      [[ "$MOUSEMODE" == "1" ]] && mouse_move 0 $MOUSESPEED || \
        $dbg $USR_CMD "DISPLAY=\":0\" /home/pi/.config/lxsession/LXDE-pi/random_wp.sh"
    if  filter_key "right" "$l"; then
      [[ "$MOUSEMODE" == "1" ]] && mouse_move $MOUSESPEED 0 
    if  filter_key "exit" "$l"; then
      $dbg $USR_CMD "pkill -SIGTERM -f lxsession"
    if  filter_key "stop" "$l"; then
d     echov   "mouse mode on"
d     osdecho "mouse mode on"
    if  filter_key "play" "$l"; then
      echov   "mouse mode off"
      osdecho "mouse mode off"

# License
# This script 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 script is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this script; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place, Suite 330,
# Boston, MA  02111-1307  USA

The script makes use of osd_cat for the visual feedback, and xdotool  for the mouse control functionality. It is recommended to install them with apt, but it’s not required because the script  will detect wether they are available or not.

As you can see on this list, each manufacturer supports the protocol to a varying degree, so you will have to play a little bit to see what button you can detect, and wether you can get the duration of the button press. For instance, on my Philips remote, I do not have access to the numbers.

Moving the mouse is not really very useful, as the latency is terrible. However, there are other cool things that you might want to associate to your remote using xdtool, for example you can read PDFs or websites binding

xdotool key Page_Down
xdotool key Page_Up
xdotool key Home
xdotool key End

, or even bind to <CTRL><TAB>  to navigate through browser tabs.

Author: nachoparker

Humbly sharing things that I find useful [ github dockerhub ]


  1. I see you own a similar remote to mine. Might be that you are using a Phillips TV. I have one and CEC doesn’t seem to work. EasyLink is enabled, but it does not respond to the `echo “on 0000” | cec-client -d 1 -s “standby 0” RPI` I send when the Pi boots. That should power up the TV

  2. hi, this doesn’t work on my Panasonic. i’ve enabled verbose mode, this is the output, do you have any ideas? thanks!!

    No device type given. Using ‘recording device’
    CEC Parser created – libCEC version 4.0.2
    no serial port given. trying autodetect:
    path: Raspberry Pi
    com port: RPI

    opening a connection to the CEC adapter…
    DEBUG: [ 53] Broadcast (F): osd name set to ‘Broadcast’
    DEBUG: [ 56] Open – vc_cec initialised
    DEBUG: [ 56] logical address changed to Free use (e)
    NOTICE: [ 56] connection opened
    DEBUG: [ 58] processor thread started
    DEBUG: [ 58] < TV (0): POLL
    DEBUG: [ 58] initiator ‘Broadcast’ is not supported by the CEC adapter. using ‘Free use’ instead
    TRAFFIC: [ 58] <> POLL sent
    DEBUG: [ 89] TV (0): device status changed into ‘present’
    DEBUG: [ 89] << requesting vendor ID of 'TV' (0)
    TRAFFIC: [ 89] <> 0f:87:00:80:45
    DEBUG: [ 298] TV (0): vendor = Panasonic (008045)
    DEBUG: [ 298] >> TV (0) -> Broadcast (F): device vendor id (87)
    DEBUG: [ 298] expected response received (87: device vendor id)
    DEBUG: [ 298] replacing the command handler for device ‘TV’ (0)
    NOTICE: [ 298] registering new CEC client – v4.0.2
    DEBUG: [ 298] detecting logical address for type ‘playback device’
    DEBUG: [ 298] trying logical address ‘Playback 1’
    DEBUG: [ 298] < Playback 1 (4): POLL
    TRAFFIC: [ 298] << 44
    TRAFFIC: [ 568] <> POLL not sent
    DEBUG: [ 839] using logical address ‘Playback 1’
    DEBUG: [ 839] Playback 1 (4): device status changed into ‘handled by libCEC’
    DEBUG: [ 839] Playback 1 (4): power status changed from ‘unknown’ to ‘on’
    DEBUG: [ 839] Playback 1 (4): vendor = Pulse Eight (001582)
    DEBUG: [ 839] Playback 1 (4): CEC version 1.4
    DEBUG: [ 839] AllocateLogicalAddresses – device ‘0’, type ‘playback device’, LA ‘4’
    DEBUG: [ 839] logical address changed to Playback 1 (4)
    DEBUG: [ 839] Playback 1 (4): osd name set to ‘CECTester’
    DEBUG: [ 839] Playback 1 (4): menu language set to ‘eng’
    DEBUG: [ 839] GetPhysicalAddress – physical address = 1000
    DEBUG: [ 839] AutodetectPhysicalAddress – autodetected physical address ‘1000’
    DEBUG: [ 839] Playback 1 (4): physical address changed from ffff to 1000
    DEBUG: [ 839] < broadcast (F): physical address 1000
    TRAFFIC: [ 839] << 4f:84:10:00:04
    NOTICE: [ 990] CEC client registered: libCEC version = 4.0.2, client version = 4.0.2, firmware version = 1, logical address(es) = Playback 1 (4) , physical address:, git revision: libcec-4.0.2+8-8563411~dirty, compiled on Sun Apr 2 17:04:01 UTC 2017 by root@hostname: Name or service not known on Linux 4.4.0-57-generic (armv7l), features: P8_USB, DRM, P8_detect, randr, RPi
    DEBUG: [ 990] Playback 1 (4): vendor = Panasonic (008045)
    DEBUG: [ 990] replacing the command handler for device 'Playback 1' (4)
    DEBUG: [ 990] < TV (0): OSD name ‘CECTester’
    TRAFFIC: [ 990] << 40:47:43:45:43:54:65:73:74:65:72
    DEBUG: [ 1290] << requesting power status of 'TV' (0)
    TRAFFIC: [ 1290] <> 04:00:47:00
    DEBUG: [ 1471] marking opcode ‘set osd name’ as unsupported feature for device ‘TV’
    DEBUG: [ 1471] >> TV (0) -> Playback 1 (4): feature abort ( 0)
    TRAFFIC: [ 1540] >> 04:90:00
    DEBUG: [ 1540] TV (0): power status changed from ‘unknown’ to ‘on’
    DEBUG: [ 1540] >> TV (0) -> Playback 1 (4): report power status (90)
    waiting for input
    DEBUG: [ 1540] expected response received (90: report power status)
    TRAFFIC: [ 2026] >> 04:8c
    DEBUG: [ 2026] < TV (0): vendor id Panasonic (8045)
    TRAFFIC: [ 2026] <> TV (0) -> Playback 1 (4): give device vendor id (8C)
    TRAFFIC: [ 2305] >> 04:89:10:01:05
    TRAFFIC: [ 2305] <> TV (0) -> Playback 1 (4): vendor command (89)
    TRAFFIC: [ 2852] >> 04:a0:00:80:45:06:05
    DEBUG: [ 2853] >> TV (0) -> Playback 1 (4): vendor command with id (A0)
    TRAFFIC: [ 4031] >> 04:a0:00:80:45:06:05
    DEBUG: [ 4031] >> TV (0) -> Playback 1 (4): vendor command with id (A0)
    TRAFFIC: [ 5206] >> 04:a0:00:80:45:06:05
    DEBUG: [ 5206] >> TV (0) -> Playback 1 (4): vendor command with id (A0)

  3. Hi – helpful! Should this work with the Pi Zero mini HDMI too? You know? And I’m just trying to build a RPi touchscreen that switches between to sources, TV and PC computer.

    Thanks for any ideas.

  4. Do you know if there’s any way to trigger this in Javascript within Chromium? Thinking about signage displays in particular, being able to schedule TV on/off with the signage application (DAKboard)

  5. The program works. However, when you make click with the OK button, it doesn’t work anymore. I realized that the text “break” from the code was the problem, if you have the same issue, just remove that text.

Leave a Reply

Your email address will not be published. Required fields are marked *