mirror of
https://github.com/theupriser/steamify-cachyos-dev.git
synced 2026-10-05 12:45:56 +02:00
feat: vmcec.sh tests HDMI-CEC against a fake TV; vmwake.sh wakes the VM over QMP
run.sh opens a QMP socket; the skill documents the CEC test and its gotchas.
This commit is contained in:
1 parent
e04db1ec5f
commit
04204f1b9d
4 files changed
+277
-21
No files matched your search
Executable
+230
@@ -0,0 +1,230 @@
|
||||
#!/bin/bash
|
||||
# Test HDMI-CEC in the guest against a fake TV: the kernel's vivid driver
|
||||
# gives a virtual HDMI input (/dev/cec0, plays the TV, driven with cec-ctl)
|
||||
# and output (/dev/cec1, the PC, where cecd runs). Needs the wizard's HDMI-CEC
|
||||
# item on (cecd, cec-ctl from v4l-utils); steamos-manager (Steam Machine
|
||||
# support) for the Steam settings tests. Prints PASS/FAIL/SKIP per check.
|
||||
#
|
||||
# scripts/vmcec.sh everything, including a real suspend/resume
|
||||
# (needs the QMP socket from run.sh to wake up)
|
||||
# scripts/vmcec.sh --no-sleep everything except suspending the VM
|
||||
# scripts/vmcec.sh --sleep-only only the real sleep/wake check (after a full run)
|
||||
#
|
||||
# Afterwards the normal cecd service runs again; vivid stays loaded.
|
||||
# Env: VM_USER (theupriser), VM_PORT (2222), VM_HOST (localhost).
|
||||
set -euo pipefail
|
||||
. "$(dirname "$0")/common.sh"
|
||||
SLEEP_TEST=true SLEEP_ONLY=false
|
||||
[[ "${1:-}" == --no-sleep ]] && SLEEP_TEST=false
|
||||
[[ "${1:-}" == --sleep-only ]] && SLEEP_ONLY=true
|
||||
|
||||
# --- in the guest: helpers, setup and all checks except the real sleep ---
|
||||
$SLEEP_ONLY || vm_ssh "SLEEP_TEST=$SLEEP_TEST bash -s" << 'REMOTE'
|
||||
export XDG_RUNTIME_DIR=/run/user/1000 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
|
||||
W=~/.cache/vmcec; mkdir -p "$W"
|
||||
pass=0 fail=0 skip=0
|
||||
PASS() { echo " PASS $*"; pass=$((pass + 1)); }
|
||||
FAIL() { echo " FAIL $*"; fail=$((fail + 1)); }
|
||||
SKIP() { echo " SKIP $*"; skip=$((skip + 1)); }
|
||||
check() { local d="$1"; shift; if "$@" >/dev/null 2>&1; then PASS "$d"; else FAIL "$d"; fi; }
|
||||
TV() { cec-ctl -d /dev/cec0 "$@"; }
|
||||
BUS=com.steampowered.CecDaemon1
|
||||
has_smgr() { steamosctl get-hdmi-cec-state >/dev/null 2>&1; }
|
||||
|
||||
# Key reader for the cecd input device: prints key names while it runs.
|
||||
cat > "$W/keys.py" << 'PY'
|
||||
import re, select, struct, sys, time
|
||||
names = {}
|
||||
for line in open('/usr/include/linux/input-event-codes.h'):
|
||||
m = re.match(r'#define\s+(KEY_\w+|BTN_\w+)\s+(0x[0-9a-fA-F]+|\d+)\b', line)
|
||||
if m: names.setdefault(int(m.group(2), 0), m.group(1))
|
||||
f = open(sys.argv[1], 'rb'); end = time.time() + float(sys.argv[2])
|
||||
while time.time() < end:
|
||||
if select.select([f], [], [], 0.1)[0]:
|
||||
_, _, t, c, v = struct.unpack('llHHi', f.read(24))
|
||||
if t == 1 and v == 1: print(names.get(c, c), flush=True)
|
||||
PY
|
||||
|
||||
# Monitor the TV side's CEC bus into $W/mon.txt (needs root).
|
||||
mon_start() { sudo pkill -f 'cec-ctl -d /dev/cec0 -M' 2>/dev/null; : > "$W/mon.txt"
|
||||
sudo sh -c "exec cec-ctl -d /dev/cec0 -M > $W/mon.txt 2>&1" & sleep 1; }
|
||||
mon_stop() { sleep 1; sudo pkill -f 'cec-ctl -d /dev/cec0 -M' 2>/dev/null; wait 2>/dev/null; }
|
||||
seen() { grep -q "Received from.*): $1" "$W/mon.txt"; }
|
||||
|
||||
echo "== Setup: fake TV (vivid)"
|
||||
lsmod | grep -q '^vivid' || sudo modprobe vivid n_devs=1 num_inputs=1 input_types=0x3 num_outputs=1 output_types=0x1
|
||||
sleep 2
|
||||
TV --tv --osd-name "Fake TV" >/dev/null 2>&1
|
||||
v4l2-ctl -d /dev/video0 --set-ctrl hdmi_000_0_is_connected_to=2
|
||||
# The cecd service (-e) takes every CEC device, the TV too: a temporary
|
||||
# drop-in limits it to the PC side, so steamos-manager still controls it.
|
||||
mkdir -p ~/.config/systemd/user/cecd.service.d
|
||||
printf '[Service]\nExecStart=\nExecStart=/usr/bin/cecd -d /dev/cec1\n' > ~/.config/systemd/user/cecd.service.d/vmcec.conf
|
||||
systemctl --user daemon-reload; systemctl --user restart cecd.service
|
||||
sleep 4
|
||||
systemctl --user is-active -q steamos-manager.service && { systemctl --user restart steamos-manager.service; sleep 3; }
|
||||
LA=$(cec-ctl -d /dev/cec1 | awk '/Logical Address /{print $4; exit}')
|
||||
PA=$(cec-ctl -d /dev/cec1 | awk '/Physical Address/{print $4; exit}')
|
||||
EV=$(grep -A4 'N: Name="cecd' /proc/bus/input/devices | grep -oE 'event[0-9]+' | head -1)
|
||||
echo " PC side: physical $PA, logical $LA, input /dev/input/${EV:-none}"
|
||||
|
||||
echo "== 1. Identity (TV asks the PC)"
|
||||
[[ "$PA" == 1.0.0.0 ]] && PASS "physical address 1.0.0.0" || FAIL "physical address is $PA"
|
||||
[[ "$LA" =~ ^(4|8|9|11)$ ]] && PASS "logical address $LA (playback device)" || FAIL "logical address is '$LA'"
|
||||
name=$(TV -t "$LA" --give-osd-name 2>&1 | awk -F': ' '/name:/{print $2; exit}')
|
||||
[[ "$name" == "Steam Machine" ]] && PASS "OSD name 'Steam Machine'" || FAIL "OSD name is '$name' (steamos-manager's config not applied?)"
|
||||
vendor=$(TV -t "$LA" --give-device-vendor-id 2>&1 | awk '/vendor-id:/{print $2; exit}')
|
||||
[[ "$vendor" == 0xe0319e* ]] && PASS "vendor ID Valve (0xe0319e)" || FAIL "vendor ID is '$vendor'"
|
||||
check "answers a poll" TV -t "$LA" --poll
|
||||
|
||||
echo "== 2. TV remote -> PC (key presses)"
|
||||
press() { TV -t "$LA" --user-control-pressed ui-cmd="$1" >/dev/null; sleep 0.15; TV -t "$LA" --user-control-released >/dev/null; sleep 0.25; }
|
||||
keys_for() { # keys_for <seconds> <ui-cmd>...: keys arriving while pressing
|
||||
EV=$(grep -A4 'N: Name="cecd' /proc/bus/input/devices | grep -oE 'event[0-9]+' | head -1)
|
||||
: > "$W/keys.txt"; local r=
|
||||
[[ -n "$EV" ]] && { sudo timeout "$1" python3 "$W/keys.py" "/dev/input/$EV" "$1" > "$W/keys.txt" & r=$!; }
|
||||
sleep 1; shift
|
||||
local k; for k in "$@"; do press "$k"; done; [[ -n "$r" ]] && wait "$r" 2>/dev/null; cat "$W/keys.txt"; }
|
||||
if [[ -z "$EV" ]]; then FAIL "no cecd input device"
|
||||
else
|
||||
for k in up down left right select exit back root-menu setup-menu contents-menu \
|
||||
play pause stop rewind fast-forward forward backward \
|
||||
volume-up volume-down mute number-entry-mode number-0-or-number-10 number-5 \
|
||||
channel-up channel-down display-information electronic-program-guide \
|
||||
f1-blue f2-red f3-green f4-yellow; do
|
||||
got=$(keys_for 2 "$k" | tr '\n' ' ')
|
||||
if [[ -n "$got" ]]; then PASS "$(printf '%-28s -> %s' "$k" "$got")"
|
||||
else SKIP "$(printf '%-28s -> nothing (not mapped by cecd)' "$k")"; fi
|
||||
done
|
||||
|
||||
# The TV's power button arrives as KEY_POWER, and Steam Machine support
|
||||
# makes the power button sleep: with sleep blocked, check the key only.
|
||||
sudo systemd-inhibit --what=sleep:handle-power-key --mode=block --who=vmcec --why=test sleep 6 & inh=$!; sleep 1
|
||||
got=$(keys_for 2 power-toggle-function | tr '\n' ' ')
|
||||
wait $inh 2>/dev/null
|
||||
[[ "$got" == *KEY_POWER* ]] && PASS "$(printf '%-28s -> %s' power-toggle-function "$got") (sleeps the PC)" ||
|
||||
FAIL "power-toggle-function -> '$got'"
|
||||
fi
|
||||
|
||||
echo "== 3. Steam settings (steamos-manager, over D-Bus like Steam)"
|
||||
# steamosctl's suspend-tv/suspend-device getters and setters don't work
|
||||
# (steamos-manager 26.4.1); Steam uses these D-Bus properties.
|
||||
SM=com.steampowered.SteamOSManager1; SMP=/com/steampowered/SteamOSManager1; CEC2=$SM.HdmiCec2
|
||||
prop() { busctl --user get-property $SM $SMP $CEC2 "$1" 2>/dev/null | awk '{print $2}'; }
|
||||
setp() { busctl --user set-property $SM $SMP $CEC2 "$1" b "$2" 2>/dev/null; sleep 2; }
|
||||
cfg() { grep -h "^$1 *=" ~/.config/cecd/config.d/*.toml 2>/dev/null | tail -1 | awk -F'= *' '{print $2}'; }
|
||||
if ! has_smgr; then SKIP "steamos-manager has no HDMI-CEC interface (no Steam Machine support?)"
|
||||
else
|
||||
echo " now: $(steamosctl get-hdmi-cec-state 2>&1); EnableControl=$(prop EnableControl) WakeTv=$(prop WakeTv) SuspendTv=$(prop SuspendTv) SuspendDevice=$(prop SuspendDevice) WakeDeviceSupported=$(prop WakeDeviceSupported)"
|
||||
setp EnableControl false
|
||||
got=$(keys_for 2 down | tr '\n' ' ')
|
||||
[[ -z "$got" && "$(cfg uinput)" == false ]] && PASS "remote control off: keys are ignored (uinput = false)" || FAIL "remote control off: keys '$got', uinput = $(cfg uinput)"
|
||||
setp EnableControl true
|
||||
got=$(keys_for 2 down | tr '\n' ' ')
|
||||
[[ -n "$got" ]] && PASS "remote control on again: $got" || FAIL "remote control on, but no keys"
|
||||
for pr in WakeTv:wake_tv SuspendTv:suspend_tv SuspendDevice:allow_standby; do
|
||||
for v in false true; do
|
||||
setp "${pr%%:*}" $v
|
||||
[[ "$(prop "${pr%%:*}")" == "$v" && "$(cfg "${pr#*:}")" == "$v" ]] &&
|
||||
PASS "${pr%%:*} $v (cecd config ${pr#*:} = $v)" || FAIL "${pr%%:*} $v: property $(prop "${pr%%:*}"), config ${pr#*:} = $(cfg "${pr#*:}")"
|
||||
done
|
||||
done
|
||||
[[ "$(prop WakeDeviceSupported)" == true ]] && PASS "waking the PC with the TV is supported" ||
|
||||
SKIP "waking the PC with the TV: not supported here (WakeDeviceSupported = false)"
|
||||
steamosctl set-hdmi-cec-state control-and-wake >/dev/null 2>&1; sleep 3
|
||||
fi
|
||||
systemctl --user is-active -q cecd.service && PASS "cecd still running" || FAIL "cecd stopped"
|
||||
|
||||
echo "== 4. PC -> TV"
|
||||
# A plain TV (no soundbar): volume goes to the TV itself (address 0).
|
||||
TV --tv --osd-name "Fake TV" >/dev/null 2>&1; sleep 2
|
||||
echo " fake TV: $(TV | awk '/Logical Address /{printf "%s%s ", $4, $5}')"
|
||||
DEV=$(busctl --user tree $BUS 2>/dev/null | grep -oE '/com/steampowered/CecDaemon1/Devices/Cec[0-9]+' | head -1)
|
||||
mon_start
|
||||
if has_smgr; then steamosctl hdmi-cec-make-active >/dev/null 2>&1; sleep 1; else busctl --user call $BUS "$DEV" $BUS.CecDevice1 SetActiveSource >/dev/null 2>&1; sleep 1; fi
|
||||
busctl --user call $BUS /com/steampowered/CecDaemon1/Daemon $BUS.Daemon1 Wake >/dev/null 2>&1; sleep 1
|
||||
mon_stop
|
||||
{ seen ACTIVE_SOURCE || seen IMAGE_VIEW_ON || seen TEXT_VIEW_ON; } && PASS "make active: TV switches to the PC ($(grep -oE '\): [A-Z_]+' "$W/mon.txt" | sort -u | tr -d '):' | tr '\n' ' '))" || FAIL "make active: TV got nothing"
|
||||
seen IMAGE_VIEW_ON || seen TEXT_VIEW_ON && PASS "wake: TV is turned on (IMAGE_VIEW_ON)" || FAIL "wake: no IMAGE_VIEW_ON"
|
||||
mon_start
|
||||
for m in VolumeUp VolumeDown Mute; do busctl --user call $BUS "$DEV" $BUS.CecDevice1 $m y 0 >/dev/null 2>&1; sleep 0.5; done
|
||||
echo " audio status: $(busctl --user --timeout=3 call $BUS "$DEV" $BUS.CecDevice1 GetAudioStatus y 0 2>&1)"; sleep 1
|
||||
mon_stop
|
||||
n=$(grep -cE 'USER_CONTROL_PRESSED' "$W/mon.txt"); ops=$(grep -A1 USER_CONTROL_PRESSED "$W/mon.txt" | grep -oiE 'volume-up|volume-down|mute' | sort -u | tr '\n' ' ')
|
||||
[[ $n -ge 3 ]] && PASS "volume up/down/mute sent to the TV ($ops)" || FAIL "volume: $n key presses reached the TV ($ops)"
|
||||
seen GIVE_AUDIO_STATUS && PASS "asks the TV's audio status" || FAIL "no GIVE_AUDIO_STATUS"
|
||||
mon_start
|
||||
busctl --user call $BUS /com/steampowered/CecDaemon1/Daemon $BUS.Daemon1 Standby b true >/dev/null 2>&1; sleep 1
|
||||
mon_stop
|
||||
seen STANDBY && PASS "standby: TV is turned off (STANDBY)" || FAIL "standby: no STANDBY"
|
||||
systemctl --user is-active -q cec-audio-control.socket && PASS "cec-audio-control socket listening" || FAIL "cec-audio-control socket not active"
|
||||
|
||||
echo "== 5. TV off -> PC to sleep (sleep blocked, so the VM stays up)"
|
||||
if ! has_smgr; then SKIP "needs steamos-manager"
|
||||
else
|
||||
TV --tv >/dev/null 2>&1; sleep 2
|
||||
for want in true false; do
|
||||
setp SuspendDevice $want; sleep 1
|
||||
since=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
sudo systemd-inhibit --what=sleep --mode=block --who=vmcec --why=test sleep 7 & inh=$!; sleep 1
|
||||
TV -t 15 --standby >/dev/null 2>&1; sleep 4
|
||||
wait $inh 2>/dev/null
|
||||
tried=$(journalctl --user -u cecd --since "$since" --no-pager | grep -c 'Failed to standby')
|
||||
if [[ $want == true ]]; then
|
||||
[[ $tried -gt 0 ]] && PASS "SuspendDevice on: TV standby makes the PC go to sleep (blocked here)" || FAIL "SuspendDevice on, but no sleep attempt"
|
||||
else
|
||||
[[ $tried -eq 0 ]] && PASS "SuspendDevice off: TV standby doesn't touch the PC" || FAIL "SuspendDevice off, but the PC tried to sleep"
|
||||
fi
|
||||
done
|
||||
setp SuspendDevice true
|
||||
fi
|
||||
|
||||
if ! $SLEEP_TEST; then rm -f ~/.config/systemd/user/cecd.service.d/vmcec.conf; systemctl --user daemon-reload; systemctl --user restart cecd.service; fi
|
||||
echo "== Result: $pass passed, $fail failed, $skip skipped (before the sleep test)"
|
||||
echo "$LA" > "$W/la"
|
||||
REMOTE
|
||||
|
||||
$SLEEP_TEST || { echo "(sleep test skipped)"; exit 0; }
|
||||
|
||||
# --- real suspend/resume: the TV should go off, and on again at wake ---
|
||||
# Through logind (systemctl suspend), like Steam and the power button: cecd
|
||||
# acts on logind's PrepareForSleep, which rtcwake skips. cec-follower plays a
|
||||
# TV that answers power status; QMP (vmwake.sh) wakes the VM.
|
||||
echo "== 6. Real sleep and wake (systemctl suspend, woken over QMP)"
|
||||
vm_ssh bash -s << 'REMOTE'
|
||||
export XDG_RUNTIME_DIR=/run/user/1000 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
|
||||
W=~/.cache/vmcec
|
||||
# cecd on the PC side only, with debug logging (it logs what it does at sleep).
|
||||
mkdir -p ~/.config/systemd/user/cecd.service.d
|
||||
printf '[Service]\nExecStart=\nExecStart=/usr/bin/cecd -d /dev/cec1\nEnvironment=RUST_LOG=debug\n' > ~/.config/systemd/user/cecd.service.d/vmcec.conf
|
||||
systemctl --user daemon-reload; systemctl --user restart cecd.service; sleep 4
|
||||
SM=com.steampowered.SteamOSManager1; SMP=/com/steampowered/SteamOSManager1
|
||||
busctl --user set-property $SM $SMP $SM.HdmiCec2 SuspendTv b true 2>/dev/null
|
||||
busctl --user set-property $SM $SMP $SM.HdmiCec2 WakeTv b true 2>/dev/null
|
||||
sudo systemctl stop vmcec-tv vmcec-mon 2>/dev/null; sudo systemctl reset-failed vmcec-tv vmcec-mon vmcec-sleep 2>/dev/null
|
||||
cec-ctl -d /dev/cec0 --tv >/dev/null; sleep 1
|
||||
sudo systemd-run -q --unit=vmcec-tv sh -c 'exec cec-follower -d /dev/cec0 > /dev/null 2>&1'; sleep 2
|
||||
steamosctl hdmi-cec-make-active >/dev/null 2>&1; sleep 3
|
||||
sudo systemd-run -q --unit=vmcec-mon sh -c "exec cec-ctl -d /dev/cec0 -M > $W/sleep-mon.txt 2>&1"; sleep 1
|
||||
sudo systemd-run -q --unit=vmcec-sleep sh -c 'sleep 2; systemctl suspend'
|
||||
echo " suspending..."
|
||||
REMOTE
|
||||
sleep 15
|
||||
"$(dirname "$0")/vmwake.sh" || true
|
||||
until vm_ssh -o ConnectTimeout=4 true 2>/dev/null; do sleep 3; done
|
||||
sleep 8
|
||||
vm_ssh bash -s << 'REMOTE'
|
||||
export XDG_RUNTIME_DIR=/run/user/1000 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
|
||||
W=~/.cache/vmcec
|
||||
sudo systemctl stop vmcec-mon vmcec-tv 2>/dev/null
|
||||
t=$(journalctl -b --no-pager -o short-iso | grep "PM: suspend entry" | tail -1 | awk '{print $1}')
|
||||
[[ -n "$t" ]] && echo " PASS the VM really slept and woke (suspend at $t)" || echo " FAIL no suspend in the kernel log"
|
||||
log=$(journalctl --user -u cecd --since "$(date -d "${t:-now} -10 sec" '+%F %T')" --no-pager)
|
||||
grep -q 'Putting TV in standby' <<< "$log" && echo " PASS cecd: 'Putting TV in standby'" || echo " FAIL cecd didn't act on going to sleep"
|
||||
grep -q 'Waking TV' <<< "$log" && echo " PASS cecd: 'Woke from standby. Waking TV.'" || echo " FAIL cecd didn't act on waking up"
|
||||
grep -q 'Received from.*: STANDBY' "$W/sleep-mon.txt" && echo " PASS going to sleep: TV turned off (STANDBY)" || echo " FAIL going to sleep: no STANDBY to the TV"
|
||||
grep -qE 'Received from.*: (IMAGE_VIEW_ON|TEXT_VIEW_ON)' "$W/sleep-mon.txt" && echo " PASS waking up: TV turned on (IMAGE_VIEW_ON)" || echo " FAIL waking up: no IMAGE_VIEW_ON"
|
||||
grep -oE '^(Received|Transmitted)[^:]*: [A-Z_]+' "$W/sleep-mon.txt" | sed 's/^/ /'
|
||||
# Back to the normal service.
|
||||
rm -f ~/.config/systemd/user/cecd.service.d/vmcec.conf; systemctl --user daemon-reload; systemctl --user restart cecd.service
|
||||
REMOTE
|
||||
Reference in new issue
Block a user