With I2C access granted, the helper still found nothing: every connected monitor was probed on the wrong bus. A connector has two. The `ddc` symlink points at the classic I2C line that carries EDID on HDMI and DVI. DisplayPort carries DDC/CI over the AUX channel instead, which appears as a child directory of the connector. Both exist on a DP connector and both resolve, so the wrong choice looks entirely reasonable and simply finds no monitor: this machine's DP-2 has ddc -> i2c-5, where ddcutil reports "No monitor detected", while its AUX child i2c-9 answers VCP 0x10 immediately. This is the guess the previous commit said was unverified, and it was wrong in the way that mattered. Enumerating from sysfs is still right -- it gives the connector name Hyprland uses and skips empty connectors -- but it has to prefer the AUX child and fall back to the symlink. The fixture now mirrors sysfs properly: /sys/class/drm/<connector> is a SYMLINK to the real device directory, and `find` does not follow the path it is given. A fixture built from plain directories passes whether or not the code resolves the symlink first, which is a test that agrees with itself rather than with the kernel. Verified on hardware: the Kuycon P20 reports 100%, accepts 70 and returns to 100, and the media keys move it through codex's OSD path. Both bus-selection mistakes are now caught by the contract. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
164 lines
7.0 KiB
Bash
Executable File
164 lines
7.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# panama-brightness enumerates monitors from sysfs and speaks DDC/CI to them.
|
|
#
|
|
# The parts worth pinning down are the ones that decide whether a slider appears
|
|
# at all, and whether it appears attached to the right screen:
|
|
#
|
|
# * only connectors with something plugged in are probed, because probing an
|
|
# empty bus costs a timeout each and there are fourteen of them here;
|
|
# * a panel that cannot report brightness is omitted rather than shown as a
|
|
# control that does nothing;
|
|
# * the connector name matches what Hyprland calls the output, since the UI
|
|
# joins on it to get the monitor's description;
|
|
# * no I2C access produces the udev command that fixes it, not "no displays".
|
|
#
|
|
# Runs entirely against fixtures. Real monitors are never touched: both the
|
|
# sysfs root and the device root are redirected, and ddcutil is replaced on PATH.
|
|
|
|
set -uo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
helper="$repo_dir/config/dot/quickshell/scripts/panama-brightness"
|
|
|
|
fail() {
|
|
printf 'brightness helper contract: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
fixture="$(mktemp -d /tmp/panama-brightness.XXXXXX)"
|
|
trap 'rm -rf "$fixture"' EXIT
|
|
|
|
mkdir -p "$fixture/drm" "$fixture/dev" "$fixture/bin" "$fixture/i2c"
|
|
|
|
# Two connectors with a monitor, two without. DP-2 answers DDC; DP-3 is
|
|
# connected but does not implement brightness. HDMI-A-1 and DP-1 are empty and
|
|
# must never be probed at all.
|
|
# A connector has an EDID bus (the `ddc` symlink) and, on DisplayPort, an AUX
|
|
# bus that appears as a child directory. DDC/CI rides the AUX channel where one
|
|
# exists, and the `ddc` line answers nothing on a DP connector even though it
|
|
# still resolves -- so the aux argument here is what a real DisplayPort monitor
|
|
# looks like, and omitting it is what HDMI and DVI look like.
|
|
make_connector() {
|
|
local name="$1" ddc_bus="$2" status="$3" aux_bus="${4:-}"
|
|
local device="$fixture/devices/$name"
|
|
|
|
# /sys/class/drm/<connector> is a SYMLINK to the real device directory, and
|
|
# this fixture mirrors that rather than using a plain directory. It matters:
|
|
# `find` does not follow the path it is given, so code that searches the
|
|
# unresolved path finds nothing while appearing to work anywhere the entry
|
|
# happens to be a real directory.
|
|
mkdir -p "$device"
|
|
printf '%s\n' "$status" >"$device/status"
|
|
mkdir -p "$fixture/i2c/i2c-$ddc_bus"
|
|
ln -sfn "$fixture/i2c/i2c-$ddc_bus" "$device/ddc"
|
|
[[ -n "$aux_bus" ]] && mkdir -p "$device/i2c-$aux_bus"
|
|
|
|
mkdir -p "$fixture/drm"
|
|
ln -sfn "$device" "$fixture/drm/$name"
|
|
return 0
|
|
}
|
|
# DP-2 is the DisplayPort case: its EDID line is bus 5, which answers nothing,
|
|
# and its AUX child is bus 9, which does. Choosing bus 5 here finds no monitor
|
|
# at all, which is exactly the bug this pins down.
|
|
make_connector card1-DP-1 4 disconnected
|
|
make_connector card1-DP-2 5 connected 9
|
|
make_connector card1-DP-3 6 connected
|
|
make_connector card1-HDMI-A-1 7 disconnected
|
|
|
|
# has_accessible_bus only needs one readable/writable node to exist.
|
|
touch "$fixture/dev/i2c-5"
|
|
|
|
# Stub ddcutil. Records every bus it is asked about so the test can prove the
|
|
# disconnected ones were skipped. Bus 6 refuses, standing in for a panel without
|
|
# VCP 0x10.
|
|
#
|
|
# Bus 5 reports its brightness out of 200 rather than 100. Most panels do use
|
|
# 100, which is exactly the problem: with a maximum of 100 the scaling
|
|
# arithmetic is the identity, so a helper that ignored the reported maximum
|
|
# entirely would pass every assertion. 200 makes reads and writes that skip the
|
|
# conversion visibly wrong.
|
|
cat >"$fixture/bin/ddcutil" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
bus=""
|
|
args=("$@")
|
|
for ((i = 0; i < ${#args[@]}; i++)); do
|
|
[[ "${args[$i]}" == "--bus" ]] && bus="${args[$((i + 1))]}"
|
|
done
|
|
printf '%s\n' "$bus" >>"$DDCUTIL_PROBE_LOG"
|
|
|
|
for arg in "$@"; do
|
|
if [[ "$arg" == "setvcp" ]]; then
|
|
printf 'set %s %s\n' "$bus" "${args[-1]}" >>"$DDCUTIL_SET_LOG"
|
|
exit 0
|
|
fi
|
|
done
|
|
|
|
case "$bus" in
|
|
9) printf 'VCP 10 C 120 200\n'; exit 0 ;;
|
|
*) exit 1 ;;
|
|
esac
|
|
STUB
|
|
chmod +x "$fixture/bin/ddcutil"
|
|
|
|
export DDCUTIL_PROBE_LOG="$fixture/probes.log"
|
|
export DDCUTIL_SET_LOG="$fixture/sets.log"
|
|
: >"$DDCUTIL_PROBE_LOG"
|
|
: >"$DDCUTIL_SET_LOG"
|
|
|
|
run_helper() {
|
|
PATH="$fixture/bin:$PATH" \
|
|
PANAMA_BRIGHTNESS_DRM_ROOT="$fixture/drm" \
|
|
PANAMA_BRIGHTNESS_DEV_ROOT="$fixture/dev" \
|
|
"$helper" "$@"
|
|
}
|
|
|
|
# ── Enumeration ──────────────────────────────────────────────────────────────
|
|
listing="$(run_helper list)"
|
|
jq -e . >/dev/null 2>&1 <<<"$listing" || fail "list did not emit JSON: $listing"
|
|
|
|
[[ "$(jq -r '.displays | length' <<<"$listing")" == "1" ]] \
|
|
|| fail "expected exactly one controllable display, got: $listing"
|
|
|
|
[[ "$(jq -r '.displays[0].connector' <<<"$listing")" == "DP-2" ]] \
|
|
|| fail "the connector name must match Hyprland's output name: $listing"
|
|
|
|
# The AUX bus, not the EDID bus its `ddc` symlink points at.
|
|
[[ "$(jq -r '.displays[0].bus' <<<"$listing")" == "9" ]] \
|
|
|| fail "the display was mapped to its EDID bus instead of its DisplayPort AUX bus, where nothing answers: $listing"
|
|
|
|
# 120 of a maximum of 200 is 60%.
|
|
[[ "$(jq -r '.displays[0].value' <<<"$listing")" == "60" ]] \
|
|
|| fail "brightness was not read as a percent of the reported maximum: $listing"
|
|
|
|
[[ "$(jq -r '.error' <<<"$listing")" == "" ]] \
|
|
|| fail "a successful listing must not carry an error: $listing"
|
|
|
|
# A connected panel that cannot report brightness is dropped, not listed.
|
|
jq -e '.displays | map(.connector) | index("DP-3") == null' >/dev/null <<<"$listing" \
|
|
|| fail 'a display without VCP 0x10 was listed as controllable'
|
|
|
|
# ── Disconnected connectors are never probed ─────────────────────────────────
|
|
if grep -qxE '4|7' "$DDCUTIL_PROBE_LOG"; then
|
|
fail "a disconnected connector was probed -- each empty bus costs a timeout: $(tr '\n' ' ' <"$DDCUTIL_PROBE_LOG")"
|
|
fi
|
|
|
|
# ── Writes scale to the reported maximum ─────────────────────────────────────
|
|
run_helper set 9 40
|
|
[[ "$(tail -1 "$DDCUTIL_SET_LOG")" == "set 9 80" ]] \
|
|
|| fail "set did not scale to the display's maximum: $(cat "$DDCUTIL_SET_LOG")"
|
|
|
|
run_helper set 9 500
|
|
[[ "$(tail -1 "$DDCUTIL_SET_LOG")" == "set 9 200" ]] \
|
|
|| fail "an out-of-range percent was not clamped: $(cat "$DDCUTIL_SET_LOG")"
|
|
|
|
# ── No I2C access explains itself ────────────────────────────────────────────
|
|
rm -f "$fixture/dev"/i2c-*
|
|
denied="$(run_helper list)"
|
|
[[ "$(jq -r '.displays | length' <<<"$denied")" == "0" ]] \
|
|
|| fail "displays were reported without I2C access: $denied"
|
|
grep -q 'udevadm' <<<"$(jq -r '.error' <<<"$denied")" \
|
|
|| fail "the no-access error must name the command that fixes it, got: $(jq -r '.error' <<<"$denied")"
|
|
|
|
printf 'brightness helper contract: PASS\n'
|