Skip to content

Instantly share code, notes, and snippets.

@literallylara
Last active December 12, 2025 19:43
Show Gist options
  • Select an option

  • Save literallylara/ce89e0eba56a46ae527de21a4bf2beb8 to your computer and use it in GitHub Desktop.

Select an option

Save literallylara/ce89e0eba56a46ae527de21a4bf2beb8 to your computer and use it in GitHub Desktop.
Linux CEC Setup + Monitoring + Remote Control Keymap for Pulse-Eight CEC Adapter or DisplayPort
#!/bin/sh
# Resources:
# - https://wiki.archlinux.org/title/HDMI-CEC
# - https://man.archlinux.org/man/extra/v4l-utils/rc_keymap.5.en
# - https://man.archlinux.org/man/extra/v4l-utils/ir-keytable.1.en
# - https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html
# - https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html
# Make sure to replace card1-HDMI-A-2 with your own connector in: cec0-daemon-autostart.rules
if [ "$(id -u)" -ne 0 ]; then
echo "This script must be run as root"
exit 1
fi
# Install dependencies
dnf install -y v4l-utils linuxconsoletools rc-tools
# Install cec-daemon
install -vDm755 cec-daemon /usr/local/bin
# Setup Pulse-Eight CEC Adapter (if present)
# - Attaches /dev/ttyACM0 to /dev/cec0
# - CEC via display port is attached automatically on linux
if lsusb -v | grep --silent Pulse-Eight; then
install -vDm644 [email protected] /etc/systemd/system/
install -vDm644 pulse8-cec-autoattach.rules /etc/udev/rules.d/
fi
# Configure CEC
{
install -vDm644 [email protected] /etc/systemd/system/
install -vDm644 cec0-daemon-autostart.rules /etc/udev/rules.d/
}
# Setup remote control keymap
{
install -vDm644 cec.toml /etc/rc_keymaps/cec.toml
ir-keytable --sysdev rc0 --read |
sed -E 's/^scancode //;s/ \(.+\)$//;s/(KEY_[A-Z0-9]+)/"\1"/' 2>/dev/null \
>>/etc/rc_keymaps/cec.toml
install -vDm644 rc0-keymap-autoapply.rules /etc/udev/rules.d/
}
# reload systemd and udev
{
systemctl daemon-reload
udevadm control --reload-rules
udevadm trigger
}
# Monitor cec daemon with:
# journalctl -f -u [email protected]"
# Monitor adapter with:
# journalctl -f -u [email protected]"
# Monitor remote control events with:
# ir-keytable --sysdev rc0 --test
#!/bin/bash
if [ "$(id -u)" -ne 0 ]; then
echo "This script must be run as root"
exit 1
fi
rules=(
/etc/udev/rules.d/pulse8-cec-autoattach.rules
/etc/udev/rules.d/cec0-daemon-autostart.rules
/etc/rc_keymaps/cec.toml
/etc/udev/rules.d/rc0-keymap-autoapply.rules
)
services=(
/etc/systemd/system/[email protected]
/etc/systemd/system/[email protected]
)
binaries=(
/usr/local/bin/cec-daemon
)
# remove rules
for path in "${rules[@]}"; do
if [ -f "$path" ]; then
echo "Removing: $path"
rm -f "$path"
else
echo "Skipping (not found): $path"
fi
done
# stop services
for path in "${services[@]}"; do
service_basename=$(basename "$path")
if [[ "$path" == *"@.service" ]]; then
search="${service_basename/@.service/@*.service}"
base="${service_basename/@.service/}"
names=$(
systemctl list-units "$search" --all |
grep "$base" | xargs | cut -d ' ' -f1
)
for name in $names; do
echo "Stopping $name"
systemctl stop "$name"
done
else
echo "Stopping $name"
systemctl stop "$name"
fi
echo "Removing: $path"
rm -f "$path"
done
# remove binaries
for path in "${binaries[@]}"; do
if [ -f "$path" ]; then
echo "Removing: $path"
rm -f "$path"
else
echo "Skipping (not found): $path"
fi
done
# reload systemd and udev
systemctl daemon-reload
udevadm control --reload-rules
#!/bin/bash
# Configures the CEC adapter as playback device
# and monitors output to set it as the active source whenever the root device selects it
CONNECTOR=$1
HOSTNAME=$(hostnamectl hostname)
EVENTS=(
"SET_STREAM_PATH"
"STANDBY"
)
function register_as_playback() {
cec-ctl "--osd-name=$HOSTNAME" --playback "--phys-addr-from-edid=/sys/class/drm/$CONNECTOR/edid"
}
function get_phys_addr() {
cec-ctl --skip-info --physical-address
}
function set_as_active_source() {
cec-ctl --skip-info --active-source "phys-addr=$1" >/dev/null
}
function on_standby() {
echo "-- STANDBY --"
}
function on_active() {
echo "-- ACTIVE --"
}
function on_unactive() {
echo "-- UNACTIVE --"
}
function on_set_stream_path() {
if [[ $1 != $(get_phys_addr) ]]; then
on_unactive
return
fi
set_as_active_source "$1"
on_active
}
function handle_event() {
local line=$1
local event=$2
if [[ "$line" != *"$event"* ]]; then return 1; fi
case $event in
"STANDBY")
on_standby
;;
"SET_STREAM_PATH")
IFS= read -r line
addr=$(echo "$line" | cut -d ':' -f2 | xargs)
_line=""
on_set_stream_path "$addr"
;;
*)
echo "Unknown event: $event"
;;
esac
return 0
}
function monitor() {
_last_line=""
cec-ctl --skip-info --monitor --ignore all,poll | while IFS= read -r _line; do
if [[ "$_line" != "$_last_line" ]]; then
for event in "${EVENTS[@]}"; do
handle_event "$_line" "$event" && break
done
fi
_last_line=$_line
done
}
register_as_playback
monitor
[[protocols]]
name = "CEC Remote Control"
protocol = "cec"
[protocols.scancodes]
# Execute the following command and insert the output here:
#
# ir-keytable --sysdev rc0 --read 2>/dev/null | sed -E 's/^scancode //;s/ \(.+\)$//;s/(KEY_[A-Z0-9]+)/"\1"/'
#
# It should look like this:
# 0x0000 = "KEY_ENTER"
# 0x0001 = "KEY_UP"
# 0x0002 = "KEY_DOWN"
# 0x0003 = "KEY_LEFT"
# 0x0004 = "KEY_RIGHT"
# ...
#
# You can then modify the events as needed
SUBSYSTEM=="cec" KERNEL=="cec0" ACTION=="add" TAG+="systemd" ENV{SYSTEMD_WANTS}="[email protected]"
[Unit]
# Should be called as "[email protected]" or similar
Description=Configure CEC adapter as playback device and monitor output to set it as the active source whenever the root device selects it
AssertPathExists=/sys/class/drm/%i/edid
BindsTo=dev-cec0.device
[Service]
Type=exec
ExecStart=/usr/local/bin/cec-daemon "%i"
Restart=always
RestartSec=5
ProtectSystem=strict
ProtectHome=true
[Unit]
# Should be called as "[email protected]" or similar
Description=Configure USB Pulse-Eight serial device at %I
ConditionPathExists=%I
[Service]
Type=forking
ExecStart=/usr/bin/inputattach --daemon --pulse8-cec %I
ProtectSystem=strict
ProtectHome=true
SUBSYSTEM=="tty" ACTION=="add" ATTRS{manufacturer}=="Pulse-Eight" ATTRS{product}=="CEC Adapter" TAG+="systemd" ENV{SYSTEMD_WANTS}="pulse8-cec-attach@$devnode.service"
SUBSYSTEM=="rc" ACTION=="add" ENV{DRV_NAME}=="cec" RUN+="/usr/bin/ir-keytable --sysdev $devpath --write /etc/rc_keymaps/cec.toml"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment