- Recommended flow:
- /home/linaro/rk3588-display-toolkit/bin/debian_display_setup.sh
- /home/linaro/rk3588-display-toolkit/bin/monitor_setup_interactive.sh
- /home/linaro/rk3588-display-toolkit/bin/pwsh_monitor_setup.ps1
- /home/linaro/rk3588-display-toolkit/bin/rk3588_audit.sh
- /home/linaro/rk3588-display-toolkit/bin/rk3588_autorandr_bootstrap.sh
- /home/linaro/rk3588-display-toolkit/bin/rk3588_dual_display_setup.sh
- /home/linaro/my_custom_display_config.sh
| Resolution (Width x Height) | Aspect Ratio | Common Name(s) | Total Pixels |
|---|---|---|---|
| 3840x1080 | 32:9 | DFHD (Dual Full HD) | 4,147,200 |
| 5120x1440 | 32:9 | DQHD (Dual QHD) | 7,372,800 |
| 2560x1080 | 21:9 | UltraWide FHD | 2,764,800 |
| 3440x1440 | 21:9 | UltraWide QHD | 4,953,600 |
| 3840x1600 | 21:9 | UWQHD+ | 6,144,000 |
| 5120x2160 | 21:9 | 5K Ultrawide (UW5K) | 11,059,200 |
| 2048x1080 | 17:9 | 2K | 2,211,840 |
| 4096x2160 | 17:9 | DCI 4K | 8,847,360 |
| 1280x800 | 16:10 | WXGA | 1,024,000 |
| 1440x900 | 16:10 | WXGA+ | 1,296,000 |
| 1680x1050 | 16:10 | WSXGA+ | 1,764,000 |
| 1920x1200 | 16:10 | WUXGA | 2,304,000 |
| 2560x1600 | 16:10 | WQXGA | 4,096,000 |
| 1280x720 | 16:9 | HD, 720p | 921,600 |
| 1360x768 | 16:9 | HD | 1,044,480 |
| 1366x768 | 16:9 | HD | 1,049,088 |
| 1536x864 | 16:9 | - | 1,327,104 |
| 1600x900 | 16:9 | HD+ | 1,440,000 |
| 1920x1080 | 16:9 | Full HD, FHD, 1080p | 2,073,600 |
| 2048x1152 | 16:9 | QWXGA | 2,359,296 |
| 2560x1440 | 16:9 | QHD, WQHD, 1440p | 3,686,400 |
| 3840x2160 | 16:9 | 4K UHD | 8,294,400 |
| 5120x2880 | 16:9 | 5K | 14,745,600 |
| 7680x4320 | 16:9 | 8K UHD | 33,177,600 |
| 1280x1024 | 5:4 | SXGA | 1,310,720 |
| 1280x768 | 5:3 | WXGA | 983,040 |
| 640x480 | 4:3 | VGA | 307,200 |
| 800x600 | 4:3 | SVGA | 480,000 |
| 1024x768 | 4:3 | XGA | 786,432 |
| 1152x864 | 4:3 | XGA+ | 995,328 |
| 1280x960 | 4:3 | SXGA- | 1,228,800 |
| 1400x1050 | 4:3 | SXGA+ | 1,470,000 |
| 1600x1200 | 4:3 | UXGA | 1,920,000 |
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# rk3588-bullseye-display-toolkit-setup-integrated.sh
#
# Target: Debian 11 (Bullseye) on ARM64 (aarch64) RK3588 Rockchip bare-metal
# Run as: normal user with sudo privileges (recommended from within ~)
#
# Purpose:
# - Install a small baseline of dependencies (idempotent)
# - Generate a display/audit toolkit under ~/rk3588-display-toolkit
# - Keep changes safe and explicit: scripts that modify display settings require confirmation
#
# This integrated build combines:
# - Installer safety (atomic writes + backups + explicit overwrite confirm)
# - Deeper audit capabilities (optional, confirmed actions; broader subsystem coverage)
# - Safer interactive display picking (SafeMode-first candidates, connector ordering)
# - Optional autorandr persistence wiring (user-level systemd/XDG autostart)
# - Optional PowerShell (pwsh) configurator (written to disk; requires pwsh installed separately)
VERSION="2025-12-18-integrated"
# -----------------------------
# Globals / configuration
# -----------------------------
TOOLKIT_DIR_DEFAULT="$HOME/rk3588-display-toolkit"
TOOLKIT_DIR="${TOOLKIT_DIR:-$TOOLKIT_DIR_DEFAULT}"
BIN_DIR="$TOOLKIT_DIR/bin"
LOG_DIR="$TOOLKIT_DIR/logs"
STATE_DIR="$TOOLKIT_DIR/state"
AUDIT_SCRIPT="$BIN_DIR/rk3588_audit.sh"
MENU_SCRIPT="$BIN_DIR/debian_display_setup.sh"
INTERACTIVE_SCRIPT="$BIN_DIR/monitor_setup_interactive.sh"
DUAL_FIX_SCRIPT="$BIN_DIR/rk3588_dual_display_setup.sh"
BOOTSTRAP_SCRIPT="$BIN_DIR/rk3588_autorandr_bootstrap.sh"
PWSH_SCRIPT="$BIN_DIR/pwsh_monitor_setup.ps1"
CONFIRM_TOKEN="${CONFIRM_TOKEN:-yes}"
NONINTERACTIVE="${NONINTERACTIVE:-0}" # if 1, will NOT overwrite existing files
AUTO_CONFIRM_OVERWRITE="${AUTO_CONFIRM_OVERWRITE:-0}" # if 1, auto-confirms overwrites (still makes backups)
TMPDIR_CREATED=""
cleanup() {
if [[ -n "${TMPDIR_CREATED:-}" && -d "$TMPDIR_CREATED" ]]; then
rm -rf "$TMPDIR_CREATED" || true
fi
}
trap cleanup EXIT SIGINT SIGTERM
# -----------------------------
# Helper functions
# -----------------------------
log() { printf '[%(%F %T)T] %s\n' -1 "$*"; }
err() { printf '[%(%F %T)T] ERROR: %s\n' -1 "$*" >&2; }
die() { err "$*"; exit 1; }
have_cmd() { command -v "$1" >/dev/null 2>&1; }
require_home_context() {
if [[ "$PWD" != "$HOME"* ]]; then
err "This script is intended to be run from within your home directory (~). Current PWD: $PWD"
err "Continuing anyway, but generated files will be written to: $TOOLKIT_DIR"
fi
}
check_platform() {
local debver arch
debver="$(cut -d'.' -f1 < /etc/debian_version 2>/dev/null || echo unknown)"
arch="$(uname -m 2>/dev/null || echo unknown)"
if [[ "$debver" != "11" ]]; then
err "Detected Debian major version '$debver' (expected 11/Bullseye). Continuing, but packages may differ."
fi
if [[ "$arch" != "aarch64" && "$arch" != "arm64" ]]; then
err "Detected architecture '$arch' (expected ARM64/aarch64). Continuing, but this is tuned for ARM64."
fi
}
need_sudo() {
if ! have_cmd sudo; then
die "sudo is required. Install sudo or run as root (not recommended for X tooling)."
fi
if ! sudo -v; then
die "sudo authentication failed."
fi
}
apt_update() {
log "Updating APT package lists..."
sudo apt-get update
}
pkg_installed() { dpkg -s "$1" >/dev/null 2>&1; }
package_available() {
local pkg="$1"
apt-cache policy "$pkg" 2>/dev/null | awk '/Candidate:/ {print $2}' | grep -vq "(none)"
}
apt_install_pkgs() {
local pkgs=("$@")
local to_install=()
for p in "${pkgs[@]}"; do
if pkg_installed "$p"; then
log "Package already installed: $p"
continue
fi
if ! package_available "$p"; then
err "Package not available in APT sources (skipping): $p"
continue
fi
to_install+=("$p")
done
if (( ${#to_install[@]} == 0 )); then
log "All requested packages are already installed (or unavailable and skipped)."
return 0
fi
log "Installing packages: ${to_install[*]}"
sudo apt-get install -y --no-install-recommends "${to_install[@]}"
}
confirm_high_risk() {
local message="$1"
if [[ "$AUTO_CONFIRM_OVERWRITE" == "1" ]]; then
log "AUTO_CONFIRM_OVERWRITE=1 set; proceeding without prompt: $message"
return 0
fi
if [[ "$NONINTERACTIVE" == "1" ]]; then
err "NONINTERACTIVE=1 set; refusing interactive confirmation for: $message"
return 1
fi
echo
echo "WARNING: $message"
read -r -p "Type '$CONFIRM_TOKEN' to continue, anything else to cancel: " ans
[[ "$ans" == "$CONFIRM_TOKEN" ]] || { err "Cancelled by user."; return 1; }
}
backup_if_exists() {
local path="$1"
if [[ -e "$path" ]]; then
local ts bak
ts="$(date +%Y%m%d_%H%M%S)"
bak="${path}.bak.${ts}"
log "Backing up existing file: $path -> $bak"
cp -a "$path" "$bak"
fi
}
safe_write_file() {
local path="$1"
local mode="${2:-644}"
mkdir -p "$(dirname "$path")"
if [[ -e "$path" ]]; then
if [[ "$NONINTERACTIVE" == "1" && "$AUTO_CONFIRM_OVERWRITE" != "1" ]]; then
err "File exists and NONINTERACTIVE=1. Skipping: $path"
return 0
fi
if ! confirm_high_risk "This will overwrite: $path (a backup will be created)."; then
err "Skipping write to $path"
return 0
fi
backup_if_exists "$path"
fi
TMPDIR_CREATED="$(mktemp -d)"
local tmpfile="$TMPDIR_CREATED/tmp.out"
cat > "$tmpfile"
install -m "$mode" "$tmpfile" "$path"
rm -rf "$TMPDIR_CREATED" || true
TMPDIR_CREATED=""
log "Wrote: $path"
}
# -----------------------------
# Step 1: Install baseline dependencies
# -----------------------------
install_dependencies() {
log "=== Step 1/4: Installing baseline dependencies ==="
need_sudo
apt_update
apt_install_pkgs \
ca-certificates curl wget \
x11-xserver-utils xrandr \
autorandr \
python3 python3-tk \
coreutils gawk bc jq \
procps pciutils usbutils
apt_install_pkgs edid-decode read-edid ddcutil inxi lshw hwinfo
if ! have_cmd timeout; then
die "'timeout' command not found after installing coreutils; unexpected."
fi
log "Dependencies installed/verified."
}
# -----------------------------
# Step 2: Generate toolkit scripts
# -----------------------------
generate_audit_script() {
log "Generating: $AUDIT_SCRIPT"
safe_write_file "$AUDIT_SCRIPT" 755 <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
set -o errtrace
IFS=$'\n\t'
trap 'echo "Error on or near line ${LINENO}; command exited with status $?" >&2' ERR
# rk3588_audit.sh
#
# Purpose:
# - Create a timestamped audit directory under ~/rk3588_audit_<timestamp>/
# - Collect system/display/GPU/VPU/network/storage snapshots
# - Install diagnostic packages idempotently (skips missing packages)
# - Avoid destructive actions; optional actions require explicit confirmation
START_TS="$(date +%Y%m%d_%H%M%S)"
AUDIT_DIR="$HOME/rk3588_audit_${START_TS}"
LOG_DIR="$AUDIT_DIR/logs"
OUT_DIR="$AUDIT_DIR/out"
REPORT_MD="$AUDIT_DIR/REPORT.md"
LOG_FILE="$LOG_DIR/audit.log"
ENV_FILE="$LOG_DIR/env.txt"
DMESG_FILE="$LOG_DIR/dmesg.txt"
CMD_FAIL_HINT="Check ${LOG_FILE} and ${REPORT_MD} for details."
: "${NET_TIMEOUT:=5}"
: "${FIO_SIZE_MB:=512}"
: "${FIO_BS:=1M}"
: "${FIO_IODEPTH:=16}"
: "${RUN_COLORS:=1}"
TMPDIR_CREATED=""
cleanup() {
if [[ -n "${TMPDIR_CREATED:-}" && -d "$TMPDIR_CREATED" ]]; then
rm -rf "$TMPDIR_CREATED" || true
fi
}
trap cleanup EXIT SIGINT SIGTERM
if [[ -t 1 && "$RUN_COLORS" -eq 1 ]] && command -v tput >/dev/null 2>&1; then
GREEN="$(tput setaf 2)"; YELLOW="$(tput setaf 3)"; RED="$(tput setaf 1)"; BLUE="$(tput setaf 4)"; BOLD="$(tput bold)"; RESET="$(tput sgr0)"
else
GREEN=""; YELLOW=""; RED=""; BLUE=""; BOLD=""; RESET=""
fi
log() { echo -e "$*" | tee -a "$LOG_FILE"; }
section() { log "\n${BOLD}${BLUE}==> $1${RESET}"; }
ensure_dir() { [[ -d "$1" ]] || mkdir -p "$1"; }
ask_yes() {
local prompt="$1" ans
echo
read -r -p "${YELLOW}${prompt}${RESET} (type 'yes' to continue, anything else to cancel): " ans
[[ "$ans" == "yes" ]] || { echo "Cancelled by user."; return 1; }
}
need_sudo() {
command -v sudo >/dev/null 2>&1 || { echo "Error: sudo required." >&2; exit 1; }
sudo -v || { echo "Error: sudo auth failed." >&2; exit 1; }
}
package_available() {
local pkg="$1"
apt-cache policy "$pkg" 2>/dev/null | awk '/Candidate:/ {print $2}' | grep -vq "(none)"
}
is_installed() { dpkg -s "$1" >/dev/null 2>&1; }
ensure_package() {
local pkg="$1"
if is_installed "$pkg"; then
log "Package already installed: ${GREEN}${pkg}${RESET}"
return 0
fi
if ! package_available "$pkg"; then
log "Package not available (skipping): ${YELLOW}${pkg}${RESET}"
return 0
fi
log "Installing package: ${GREEN}${pkg}${RESET}"
sudo apt-get install -y --no-install-recommends "$pkg" >>"$LOG_FILE" 2>&1 || {
echo "Error: Failed to install '$pkg'. ${CMD_FAIL_HINT}" >&2
exit 1
}
}
ensure_packages() { for pkg in "$@"; do ensure_package "$pkg"; done; }
run_continue() {
local title="$1"; shift
log "\n--- $title ---"
"$@" >>"$LOG_FILE" 2>&1 || log " (Command failed but continuing): $*"
}
run_fail() {
local title="$1"; shift
log "\n>>> $title"
"$@" >>"$LOG_FILE" 2>&1 || { echo "Error: $title failed. ${CMD_FAIL_HINT}" >&2; exit 1; }
}
quick_net_check() {
section "Quick network check"
if command -v ping >/dev/null 2>&1 && ping -c1 -W "$NET_TIMEOUT" deb.debian.org >/dev/null 2>&1; then
log "Network reachable."
return 0
fi
log "Network check failed; installs/downloads may be limited."
return 1
}
check_platform() {
section "Platform checks"
local debver arch
debver="$(cut -d'.' -f1 < /etc/debian_version 2>/dev/null || echo unknown)"
arch="$(uname -m 2>/dev/null || echo unknown)"
log "Debian major: $debver"
log "Arch : $arch"
[[ "$debver" == "11" ]] || log "WARNING: tuned for Debian 11 (Bullseye)."
[[ "$arch" == "aarch64" || "$arch" == "arm64" ]] || log "WARNING: tuned for ARM64."
}
enable_nonfree_optional() {
section "Optional: Enable contrib/non-free (Bullseye)"
if ! ask_yes "Enable 'contrib non-free' in /etc/apt/sources.list (backup + apt update)?"; then
log "Skipped enabling contrib/non-free."
return 0
fi
local src="/etc/apt/sources.list"
if [[ ! -f "$src" ]]; then
log "No $src found; skipping."
return 0
fi
sudo cp -a "$src" "${src}.bak.${START_TS}"
TMPDIR_CREATED="$(mktemp -d)"
local tmp="$TMPDIR_CREATED/sources.list"
sudo awk '{
if ($1=="deb" || $1=="deb-src") {
line=$0
has_contrib=match(line,/(^| )contrib( |$)/)
has_nonfree=match(line,/(^| )non-free( |$)/)
if (!has_contrib) line=line" contrib"
if (!has_nonfree) line=line" non-free"
print line
} else {
print
}
}' "$src" | sudo tee "$tmp" >/dev/null
sudo mv "$tmp" "$src"
run_fail "apt-get update (after enabling contrib/non-free)" sudo apt-get update
rm -rf "$TMPDIR_CREATED" || true
TMPDIR_CREATED=""
}
main() {
ensure_dir "$AUDIT_DIR"; ensure_dir "$LOG_DIR"; ensure_dir "$OUT_DIR"
: >"$LOG_FILE"
section "Start"
log "Audit directory: $AUDIT_DIR"
log "Log file : $LOG_FILE"
need_sudo
{
echo "===== ENVIRONMENT ====="
echo "Timestamp: $START_TS"
uname -a || true
echo
echo "----- /etc/os-release -----"
cat /etc/os-release 2>/dev/null || true
echo
echo "----- /proc/cmdline -----"
cat /proc/cmdline 2>/dev/null || true
echo
echo "----- CPU -----"
lscpu 2>/dev/null || true
echo
echo "----- Memory/CMA -----"
grep -E 'CmaTotal|CmaFree|MemTotal|MemFree|HugePages' /proc/meminfo 2>/dev/null || true
} >"$ENV_FILE"
run_continue "Collect dmesg" bash -lc "sudo dmesg -T > '$DMESG_FILE'"
check_platform
quick_net_check || true
section "APT update"
run_fail "apt-get update" sudo apt-get update
enable_nonfree_optional || true
section "Install baseline diagnostic tools (idempotent; skips unavailable)"
ensure_packages \
curl wget ca-certificates \
pciutils usbutils lshw hwinfo inxi \
ethtool iproute2 net-tools jq \
i2c-tools lm-sensors \
v4l-utils \
gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly \
ffmpeg alsa-utils \
vulkan-tools mesa-utils kmscube \
xrandr x11-xserver-utils autorandr \
edid-decode read-edid ddcutil \
fio hdparm nvme-cli smartmontools \
iw wireless-tools bluez can-utils
section "System overview"
run_continue "lshw (short)" sudo lshw -short
run_continue "lsblk -O" lsblk -O
run_continue "df -hT" df -hT
run_continue "lsusb -t" lsusb -t
run_continue "lspci -nnk" lspci -nnk
run_continue "Kernel warnings/errors (last 200)" bash -lc 'dmesg -T --level=err,warn | tail -n 200'
section "GPU & Display"
run_continue "GPU modules loaded" bash -lc "lsmod | egrep -i 'panthor|panfrost|mali|kbase' || true"
run_continue "GPU-related dmesg" bash -lc "dmesg -T | egrep -i 'mali|panthor|panfrost|csf|gpu' || true"
run_continue "DRM connectors (modetest -c)" modetest -c
run_continue "kmscube smoke test" bash -lc "kmscube -i 100 >/dev/null 2>&1 || true"
run_continue "Vulkan summary" vulkaninfo --summary
run_continue "xrandr --props (if X running)" bash -lc "DISPLAY=\${DISPLAY:-:0} xrandr --props 2>/dev/null || true"
section "Video / V4L2 / Codecs"
run_continue "List V4L2 devices" v4l2-ctl --list-devices
run_continue "FFmpeg hwaccels" ffmpeg -hide_banner -hwaccels
run_continue "GStreamer rockchip-ish plugins" bash -lc "gst-inspect-1.0 | egrep -i 'v4l2|rkv|hantro|rockchip' || true"
section "Audio"
run_continue "ALSA playback" aplay -l
run_continue "ALSA capture" arecord -l
section "Network"
run_continue "ip -details addr" ip -details address
run_continue "iw dev" iw dev
section "Storage"
run_continue "lsblk (model/serial)" lsblk -o NAME,SIZE,TYPE,MOUNTPOINTS,MODEL,SERIAL,TRAN
run_continue "SATA/NVMe/PCIe dmesg" bash -lc "dmesg -T | egrep -i 'sata|ahci|nvme|pcie' || true"
run_continue "nvme list (if any)" bash -lc "ls /dev/nvme*n1 >/dev/null 2>&1 && sudo nvme list || true"
section "Optional actions"
if ask_yes "Run powertop --auto-tune (changes power tunables until reboot)?"; then
run_fail "powertop --auto-tune" sudo powertop --auto-tune
fi
if ask_yes "Run sensors-detect (interactive; may load modules)?"; then
run_fail "sensors-detect" sudo sensors-detect
fi
if ask_yes "Run quick fio seq read/write in $HOME (~${FIO_SIZE_MB}MB temp file, then removed)?"; then
TMPDIR_CREATED="$(mktemp -d)"
local fiofile="$TMPDIR_CREATED/fio_test.dat"
run_fail "dd create file" dd if=/dev/zero of="$fiofile" bs=1M count="$FIO_SIZE_MB" status=none
run_fail "fio seq rw" fio --name=seqrw --filename="$fiofile" --rw=readwrite --bs="$FIO_BS" --direct=1 --numjobs=1 --iodepth="$FIO_IODEPTH" --size="${FIO_SIZE_MB}M" --group_reporting
rm -rf "$TMPDIR_CREATED" || true
TMPDIR_CREATED=""
fi
section "REPORT.md"
{
echo "# RK3588 Capability Audit — $START_TS"
echo
echo "Audit directory: $AUDIT_DIR"
echo "Log file: $LOG_FILE"
echo "Env snapshot: $ENV_FILE"
echo "dmesg: $DMESG_FILE"
} >"$REPORT_MD"
( cd "$HOME" && tar czf "${AUDIT_DIR}.tar.gz" "$(basename "$AUDIT_DIR")" )
section "Done"
log "Report : $REPORT_MD"
log "Archive: ${AUDIT_DIR}.tar.gz"
}
main "$@"
EOF
}
generate_menu_script() {
log "Generating: $MENU_SCRIPT"
safe_write_file "$MENU_SCRIPT" 755 <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# debian_display_setup.sh
# Menu-driven helper for:
# - One-time package install
# - Monitor info collection (xrandr + sysfs EDID + optional root-only tools)
# - Custom xrandr script template
# - Autorandr profile save
MONITOR_INFO_OUTDIR=""
CUSTOM_XRANDR_SCRIPT_PATH="${HOME}/my_custom_display_config.sh"
TOOLKIT_HOME="${HOME}/rk3588-display-toolkit"
CONFIG_DIR="${TOOLKIT_HOME}/state/debian_display_master"
PREP_DONE_FLAG="${CONFIG_DIR}/system_prep_done.flag"
log() { printf '[%(%F %T)T] %s\n' -1 "$*"; }
ensure_config_dir() {
mkdir -p "$CONFIG_DIR"
if [[ $EUID -eq 0 && -n "${SUDO_USER:-}" && "${SUDO_USER}" != "root" ]]; then
chown -R "$SUDO_USER:$(id -gn "$SUDO_USER")" "$CONFIG_DIR" 2>/dev/null || true
fi
}
require_root_or_return() {
if [[ $EUID -ne 0 ]]; then
echo "ERROR: This step must be run with sudo/root." >&2
echo "Re-run with: sudo bash $0" >&2
return 1
fi
return 0
}
system_prep() {
echo
echo "--- Stage 1: One-Time System Preparation (sudo required) ---"
require_root_or_return || return 1
echo ">>> Updating apt lists..."
apt-get update
echo ">>> Installing autorandr + tools..."
apt-get install -y --no-install-recommends \
autorandr xrandr x11-xserver-utils python3 python3-tk \
read-edid ddcutil hwinfo inxi lshw edid-decode bc
if systemctl list-unit-files | grep -q '^autorandr\.service'; then
systemctl enable --now autorandr.service || true
fi
if systemctl list-unit-files | grep -q '^autorandr-resume\.service'; then
systemctl enable --now autorandr-resume.service || true
fi
ensure_config_dir
date > "$PREP_DONE_FLAG"
echo ">>> System preparation complete."
}
_mi_setup_outdir() {
local real_user home_base
if [[ $EUID -eq 0 && -n "${SUDO_USER:-}" && "$SUDO_USER" != "root" ]]; then
real_user="$SUDO_USER"
else
real_user="$(whoami)"
fi
home_base="$(getent passwd "$real_user" | cut -d: -f6)"
[[ -n "$home_base" && -d "$home_base" ]] || home_base="/tmp"
MONITOR_INFO_OUTDIR="${home_base}/monitor-info-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$MONITOR_INFO_OUTDIR"
: >"$MONITOR_INFO_OUTDIR/summary.txt"
if [[ $EUID -eq 0 && -n "${SUDO_USER:-}" ]]; then
chown -R "$SUDO_USER:$(id -gn "$SUDO_USER")" "$MONITOR_INFO_OUTDIR" 2>/dev/null || true
fi
}
_mi_log_cmd() {
local logfile="$1"; shift
{
echo "===== $(date '+%F %T') : $* ====="
"$@" 2>&1 || echo "(ERROR: '$*' failed code $?)"
echo
} >>"$MONITOR_INFO_OUTDIR/$logfile"
}
_mi_detect_connectors_sysfs() {
local regex='^[a-zA-Z]+(-[a-zA-Z0-9]+)*-[0-9]+$'
echo "--- DRM connectors (sysfs) ---" >>"$MONITOR_INFO_OUTDIR/summary.txt"
for card in /sys/class/drm/card*; do
[[ -d "$card" ]] || continue
for p in "$card"/*; do
[[ -d "$p" ]] || continue
local name
name="$(basename "$p")"
if [[ "$name" =~ $regex ]]; then
echo "DRM connector: $name" >>"$MONITOR_INFO_OUTDIR/summary.txt"
fi
done
done
}
_mi_collect_sysfs_edid() {
command -v edid-decode >/dev/null 2>&1 || return 0
echo "--- EDID decode (sysfs) ---" >>"$MONITOR_INFO_OUTDIR/summary.txt"
while IFS= read -r edid; do
[[ -s "$edid" ]] || continue
_mi_log_cmd "edid_sysfs.log" edid-decode "$edid"
done < <(find /sys/class/drm -name edid 2>/dev/null || true)
}
_mi_generate_cvt_interactive() {
if ! command -v cvt >/dev/null 2>&1; then
echo "Note: 'cvt' not found; skipping modeline generator." >>"$MONITOR_INFO_OUTDIR/summary.txt"
return 0
fi
echo
read -r -p "Generate a modeline via cvt? Enter 'width height refresh' (or Enter to skip): " W H R
if [[ -n "${W:-}" && -n "${H:-}" && -n "${R:-}" ]]; then
cvt "$W" "$H" "$R" | tee -a "$MONITOR_INFO_OUTDIR/cvt_modeline.log" >/dev/null
echo "Wrote: $MONITOR_INFO_OUTDIR/cvt_modeline.log"
fi
}
monitor_info() {
echo
echo "--- Stage 2: Collect Monitor Information ---"
_mi_setup_outdir
echo "Collecting monitor information into: $MONITOR_INFO_OUTDIR"
_mi_detect_connectors_sysfs
if command -v xrandr >/dev/null 2>&1; then
_mi_log_cmd "xrandr_verbose.log" xrandr --verbose
xrandr | tee -a "$MONITOR_INFO_OUTDIR/summary.txt" >/dev/null || true
else
echo "xrandr not found. Run Stage 1 first." | tee -a "$MONITOR_INFO_OUTDIR/summary.txt" >/dev/null
fi
_mi_collect_sysfs_edid
if [[ $EUID -eq 0 ]]; then
command -v ddcutil >/dev/null 2>&1 && _mi_log_cmd "ddcutil_detect.log" ddcutil detect --verbose || true
command -v get-edid >/dev/null 2>&1 && _mi_log_cmd "get_edid.log" get-edid || true
command -v hwinfo >/dev/null 2>&1 && _mi_log_cmd "hwinfo_monitor.log" hwinfo --monitor || true
command -v lshw >/dev/null 2>&1 && _mi_log_cmd "lshw_display.log" lshw -C display || true
else
echo "Note: root-only tools (ddcutil, get-edid, hwinfo, lshw) are skipped unless run with sudo." | tee -a "$MONITOR_INFO_OUTDIR/summary.txt" >/dev/null
fi
_mi_generate_cvt_interactive
echo "Done. Summary: $MONITOR_INFO_OUTDIR/summary.txt"
}
create_or_edit_custom_xrandr() {
echo
echo "--- Stage 3: Create/Edit Custom Xrandr Configuration Script ---"
if [[ ! -f "$CUSTOM_XRANDR_SCRIPT_PATH" ]]; then
cat >"$CUSTOM_XRANDR_SCRIPT_PATH" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
# Custom Xrandr Configuration Script
# Edit this file to match your connector names and desired modes.
command -v xrandr >/dev/null 2>&1 || { echo "Error: xrandr not found" >&2; exit 1; }
echo "Applying custom xrandr settings..."
# Example:
# OUTPUT_1_NAME="HDMI-1"
# MODE_1_NAME="1920x1080"
# xrandr --output "$OUTPUT_1_NAME" --mode "$MODE_1_NAME" --primary
# Safe fallback:
xrandr --auto
SH
chmod +x "$CUSTOM_XRANDR_SCRIPT_PATH"
echo "Created template: $CUSTOM_XRANDR_SCRIPT_PATH"
else
echo "Custom script exists: $CUSTOM_XRANDR_SCRIPT_PATH"
fi
read -r -p "Open with nano now? (y/N): " ans
if [[ "$ans" =~ ^[Yy]$ ]]; then
nano "$CUSTOM_XRANDR_SCRIPT_PATH"
fi
}
test_custom_xrandr() {
echo
echo "--- Stage 4: Test Custom Xrandr Script (must be in X session) ---"
if [[ -z "${DISPLAY:-}" ]]; then
echo "ERROR: DISPLAY is not set. Run from an active X session." >&2
return 1
fi
if [[ $EUID -eq 0 ]]; then
echo "WARNING: Running xrandr as root is not recommended. Prefer running as your user." >&2
fi
if ! bash "$CUSTOM_XRANDR_SCRIPT_PATH"; then
echo "Error: custom xrandr script failed." >&2
echo "Recovery hint: switch to a TTY (Ctrl+Alt+F2), then try: DISPLAY=:0 xrandr --auto (if possible), or reboot." >&2
return 1
fi
echo "Custom xrandr script executed."
}
save_autorandr_profile() {
echo
echo "--- Stage 5: Save Autorandr Profile (run in X session) ---"
command -v autorandr >/dev/null 2>&1 || { echo "Error: autorandr not found. Run Stage 1." >&2; return 1; }
read -r -p "Enter profile name: " pname
pname="$(echo "$pname" | tr -s ' /\:&?' '_')"
[[ -n "$pname" ]] || { echo "No profile name provided." >&2; return 1; }
autorandr --save "$pname"
echo "Saved autorandr profile: $pname"
}
main_menu() {
ensure_config_dir
echo
echo "Debian Bullseye Display Setup & Management Utility"
echo "=================================================="
echo "NOTE: Some actions require sudo, others require an active X session."
PS3="Choose an option: "
local options=(
"1) One-time system preparation (sudo required)"
"2) Collect monitor information"
"3) Create/edit custom xrandr script"
"4) Test custom xrandr script (X session)"
"5) Save autorandr profile (X session)"
"6) Exit"
)
select opt in "${options[@]}"; do
case "$REPLY" in
1) system_prep ;;
2) monitor_info ;;
3) create_or_edit_custom_xrandr ;;
4) test_custom_xrandr ;;
5) save_autorandr_profile ;;
6) echo "Exiting."; exit 0 ;;
*) echo "Invalid option." ;;
esac
echo
read -r -p "Press Enter to return to the menu..." _
done
}
main_menu
EOF
}
generate_interactive_script() {
log "Generating: $INTERACTIVE_SCRIPT"
safe_write_file "$INTERACTIVE_SCRIPT" 755 <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'
# monitor_setup_interactive.sh
#
# Safer interactive mode picker with visual verification:
# - SafeMode-first: tries a short list of common resolutions first
# - Output ordering: HDMI/DP/DVI/eDP/VGA then the rest
# - Tk overlay: draws a red border on the active monitor
#
# WARNING: This script changes display modes.
# Recovery: switch to a TTY (Ctrl+Alt+F2) and run: DISPLAY=:0 xrandr --auto (if possible) or reboot.
LOGF="${LOGF:-/tmp/monitor-setup.log}"
exec > >(tee -a "$LOGF") 2>&1
log() { printf '[%(%F %T)T] %s\n' -1 "$*"; }
fail() { log "ERROR: $*"; exit 1; }
need() { command -v "$1" >/dev/null 2>&1 || fail "$1 not found"; }
need xrandr
need python3
need timeout
python3 - <<'PY' 2>/dev/null || fail "python3-tk not available. Install python3-tk."
import tkinter
PY
[[ -n "${DISPLAY:-}" ]] || fail "DISPLAY is not set. Run from an active X session."
SAFEMODES=(
1920x1080 1680x1050 1600x900 1440x900 1366x768 1280x1024 1280x960
1280x800 1280x720 1024x768 800x600 640x480
)
get_outputs() { xrandr --query | awk '/ connected/{print $1}'; }
sort_outputs() {
local -a outs
mapfile -t outs < <(get_outputs)
local prefs=(HDMI DP DVI eDP VGA)
for p in "${prefs[@]}"; do
for o in "${outs[@]}"; do [[ "$o" == ${p}* ]] && echo "$o"; done
done
for o in "${outs[@]}"; do
local hit=0
for p in "${prefs[@]}"; do [[ "$o" == ${p}* ]] && hit=1; done
((hit==0)) && echo "$o"
done
}
get_modes_for_output() {
local out="$1"
xrandr --query | awk -v o="$out" '
$1==o {inblock=1; next}
/^[A-Z]/ {inblock=0}
inblock && $1 ~ /^[0-9]+x[0-9]+/ {print $1}
' | awk '!seen[$0]++'
}
current_mode_for_output() {
local out="$1"
xrandr --query | awk -v o="$out" '
$1==o {inblock=1; next}
/^[A-Z]/ {inblock=0}
inblock && $1 ~ /^[0-9]+x[0-9]+/ {
if ($0 ~ /\*/) {print $1; exit}
}
'
}
pick_safelist() {
local -a avail=()
mapfile -t avail
local -a picked=()
for safe in "${SAFEMODES[@]}"; do
for m in "${avail[@]}"; do
[[ "$m" == "$safe" ]] && { picked+=("$m"); break; }
done
(( ${#picked[@]} >= 4 )) && break
done
if (( ${#picked[@]} > 0 )); then
printf '%s\n' "${picked[@]}"
else
printf '%s\n' "${avail[@]}" | head -n 4
fi
}
current_geometry() {
local out="$1"
xrandr --query | awk -v o="$out" '
$1==o { if (match($0, /[0-9]+x[0-9]+\+[0-9]+\+[0-9]+/)) { print substr($0, RSTART, RLENGTH) } }
'
}
show_rectangle() {
local W="$1" H="$2" X="$3" Y="$4"
log " overlay ${W}x${H}+${X}+${Y} for 5s"
timeout 7s python3 - "$W" "$H" "$X" "$Y" <<'PY'
import sys, tkinter as t
W,H,X,Y = map(int, sys.argv[1:])
root = t.Tk()
root.overrideredirect(True)
root.geometry(f"{W}x{H}+{X}+{Y}")
frame = t.Frame(root, width=W, height=H, highlightbackground='red', highlightthickness=8)
frame.pack()
root.attributes('-topmost', True)
root.after(5000, root.quit)
root.mainloop()
try:
root.destroy()
except Exception:
pass
PY
}
ask_ynq() {
local prompt="$1" ch
while true; do
printf '%s' "$prompt" > /dev/tty
IFS= read -r -n1 ch < /dev/tty || ch=""
printf '\n' > /dev/tty
case "$ch" in
y|Y) return 0;;
n|N) return 1;;
q|Q) return 2;;
*) prompt="Please type y (accept), n (next), or q (quit): ";;
esac
done
}
restore_mode_best_effort() {
local out="$1" prev="$2"
if [[ -n "$prev" ]]; then
xrandr --output "$out" --mode "$prev" >/dev/null 2>&1 || true
else
xrandr --output "$out" --auto >/dev/null 2>&1 || true
fi
}
try_modes_for_output() {
local out="$1"; shift
local -a modes=("$@")
local prev
prev="$(current_mode_for_output "$out" || true)"
for mode in "${modes[@]}"; do
log "Trying $out -> $mode"
if ! xrandr --output "$out" --mode "$mode"; then
log "Failed to set $mode on $out"
continue
fi
sleep 0.4
local geom
geom="$(current_geometry "$out" || true)"
if [[ "$geom" =~ ([0-9]+)x([0-9]+)\+([0-9]+)\+([0-9]+) ]]; then
show_rectangle "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}" "${BASH_REMATCH[4]}"
if ask_ynq "Keep $mode for $out? [y/n/q] "; then
log "Accepted $mode for $out"
return 0
else
local rc=$?
if ((rc==2)); then
restore_mode_best_effort "$out" "$prev"
return 2
fi
fi
else
log "Could not parse geometry for $out after applying $mode (continuing)."
fi
done
restore_mode_best_effort "$out" "$prev"
return 1
}
log "Starting interactive monitor configuration..."
mapfile -t outputs < <(sort_outputs)
(( ${#outputs[@]} > 0 )) || fail "No connected outputs detected by xrandr."
for out in "${outputs[@]}"; do
log "Configuring $out..."
mapfile -t avail < <(get_modes_for_output "$out")
(( ${#avail[@]} > 0 )) || { log "No modes found for $out (skipping)."; continue; }
mapfile -t testlist < <(printf '%s\n' "${avail[@]}" | pick_safelist)
if try_modes_for_output "$out" "${testlist[@]}"; then
continue
else
rc=$?
((rc==2)) && { log "Quit requested."; exit 0; }
fi
if ask_ynq "No SafeMode accepted for $out. Try ALL modes (riskier)? [y/n/q] "; then
if try_modes_for_output "$out" "${avail[@]}"; then
continue
fi
else
rc=$?
((rc==2)) && { log "Quit requested."; exit 0; }
fi
done
log "Done. For persistence, save an autorandr profile using debian_display_setup.sh."
EOF
}
generate_dual_fix_script() {
log "Generating: $DUAL_FIX_SCRIPT"
safe_write_file "$DUAL_FIX_SCRIPT" 755 <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# rk3588_dual_display_setup.sh
#
# Hard-coded dual-monitor fix (HDMI + DP) with autorandr persistence.
#
# OUT1 (default HDMI-1): 800x600@60 primary
# OUT2 (default DP-1) : 640x480@60 right-of OUT1
# Includes VGA fallback timing for OUT2.
#
# WARNING: This script changes display modes.
OUT1="${OUT1:-HDMI-1}"
OUT2="${OUT2:-DP-1}"
MODE1_NAME="800x600_60.00"
MODE1_LINE="38.25 800 832 912 1024 600 603 607 624 -hsync +vsync"
MODE2_CVT_NAME="640x480_60.00"
MODE2_CVT_LINE="23.75 640 664 720 800 480 483 487 500 -hsync +vsync"
MODE2_VGA_NAME="640x480_60_std"
MODE2_VGA_LINE="25.175 640 656 752 800 480 490 492 525 -hsync -vsync"
AR_PROFILE="rk3588_dual"
log() { printf '[INFO ] %s\n' "$*"; }
err() { printf '[ERROR] %s\n' "$*" >&2; }
need_x() {
[[ -n "${DISPLAY:-}" ]] || { err "DISPLAY not set. Run from an active X session."; exit 1; }
}
need_cmd() {
command -v "$1" >/dev/null 2>&1 || { err "Missing command: $1"; exit 1; }
}
confirm_yes() {
local msg="$1" ans
echo
echo "WARNING: $msg"
read -r -p "Type 'yes' to continue, anything else to cancel: " ans
[[ "$ans" == "yes" ]] || { err "Cancelled by user."; exit 1; }
}
apply_layout() {
local right_mode="$1"
xrandr \
--output "$OUT1" --mode "$MODE1_NAME" --primary \
--output "$OUT2" --mode "$right_mode" --right-of "$OUT1"
}
main() {
need_x
need_cmd xrandr
need_cmd autorandr
confirm_yes "This will change active display modes for $OUT1 and $OUT2. Ensure you can recover via TTY."
log "Defining modelines (best-effort)..."
xrandr --newmode "$MODE1_NAME" $MODE1_LINE 2>/dev/null || true
xrandr --newmode "$MODE2_CVT_NAME" $MODE2_CVT_LINE 2>/dev/null || true
xrandr --newmode "$MODE2_VGA_NAME" $MODE2_VGA_LINE 2>/dev/null || true
xrandr --addmode "$OUT1" "$MODE1_NAME" 2>/dev/null || true
xrandr --addmode "$OUT2" "$MODE2_CVT_NAME" 2>/dev/null || true
xrandr --addmode "$OUT2" "$MODE2_VGA_NAME" 2>/dev/null || true
log "Applying layout (try CVT timing first)..."
if ! apply_layout "$MODE2_CVT_NAME"; then
log "CVT timing failed; trying VGA fallback..."
apply_layout "$MODE2_VGA_NAME"
fi
log "Creating autorandr preswitch hook for persistence..."
local ar_dir="$HOME/.config/autorandr/$AR_PROFILE"
mkdir -p "$ar_dir"
cat >"$ar_dir/preswitch" <<SH
#!/bin/sh
xrandr --newmode "$MODE1_NAME" $MODE1_LINE 2>/dev/null || true
xrandr --newmode "$MODE2_CVT_NAME" $MODE2_CVT_LINE 2>/dev/null || true
xrandr --newmode "$MODE2_VGA_NAME" $MODE2_VGA_LINE 2>/dev/null || true
xrandr --addmode "$OUT1" "$MODE1_NAME" 2>/dev/null || true
xrandr --addmode "$OUT2" "$MODE2_CVT_NAME" 2>/dev/null || true
xrandr --addmode "$OUT2" "$MODE2_VGA_NAME" 2>/dev/null || true
SH
chmod +x "$ar_dir/preswitch"
log "Saving autorandr profile '$AR_PROFILE' (forced)..."
autorandr --save "$AR_PROFILE" --force
log "Done. Profile saved: $AR_PROFILE"
}
main "$@"
EOF
}
generate_bootstrap_script() {
log "Generating: $BOOTSTRAP_SCRIPT"
safe_write_file "$BOOTSTRAP_SCRIPT" 755 <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# rk3588_autorandr_bootstrap.sh
#
# Purpose:
# - Ensure autorandr/xrandr tooling exists
# - Help select a profile
# - Install a preswitch hook template
# - Optionally enable autorandr at login via:
# * XDG autostart (desktop environments)
# * systemd user unit
# - Optionally enable system-wide autorandr services if present
AUTORANDR_PROFILE="${AUTORANDR_PROFILE:-}"
TOOLKIT_HOME="${TOOLKIT_HOME:-$HOME/rk3588-display-toolkit}"
log() { printf '[%(%F %T)T] %s\n' -1 "$*"; }
err() { printf '[%(%F %T)T] ERROR: %s\n' -1 "$*" >&2; }
have_cmd() { command -v "$1" >/dev/null 2>&1; }
confirm_yes() {
local msg="$1" ans
echo
echo "WARNING: $msg"
read -r -p "Type 'yes' to continue, anything else to cancel: " ans
[[ "$ans" == "yes" ]] || { err "Cancelled by user."; return 1; }
}
pkg_installed() { dpkg -s "$1" >/dev/null 2>&1; }
package_available() { apt-cache policy "$1" 2>/dev/null | awk '/Candidate:/ {print $2}' | grep -vq "(none)"; }
apt_install_if_missing() {
local pkg="$1"
if pkg_installed "$pkg"; then
log "Package already installed: $pkg"
return 0
fi
if ! package_available "$pkg"; then
err "Package not available in APT sources (skipping): $pkg"
return 0
fi
log "Installing package: $pkg"
sudo apt-get install -y --no-install-recommends "$pkg"
}
ensure_deps() {
log "Ensuring dependencies..."
sudo apt-get update
apt_install_if_missing autorandr
apt_install_if_missing x11-xserver-utils
apt_install_if_missing xrandr
}
choose_profile() {
if [[ -n "$AUTORANDR_PROFILE" ]]; then
return 0
fi
have_cmd autorandr || { err "autorandr not installed"; exit 1; }
log "Available autorandr profiles:"
local profiles
profiles="$(autorandr --list 2>/dev/null || true)"
if [[ -z "$profiles" ]]; then
err "No autorandr profiles found. Create one first (e.g., using debian_display_setup.sh Stage 5)."
exit 1
fi
echo "$profiles" | sed 's/^/ - /'
read -r -p "Enter the profile name to use: " AUTORANDR_PROFILE
[[ -n "$AUTORANDR_PROFILE" ]] || { err "No profile provided"; exit 1; }
}
install_preswitch_hook() {
log "Installing preswitch hook template for profile: $AUTORANDR_PROFILE"
local ar_dir="$HOME/.config/autorandr/$AUTORANDR_PROFILE"
mkdir -p "$ar_dir"
local hook="$ar_dir/preswitch"
cat >"$hook" <<'SH'
#!/bin/sh
# autorandr preswitch hook (template)
# Runs before switching to the profile.
# Use this to (re-)add custom modelines if your hardware/firmware forgets them.
# Example:
# xrandr --newmode "1920x1080_60.00" ... || true
# xrandr --addmode HDMI-1 "1920x1080_60.00" || true
exit 0
SH
chmod +x "$hook"
log "Hook installed: $hook"
}
install_xdg_autostart() {
local auto_dir="$HOME/.config/autostart"
local desktop_file="$auto_dir/autorandr-change.desktop"
mkdir -p "$auto_dir"
if [[ -f "$desktop_file" ]]; then
if ! confirm_yes "This will overwrite $desktop_file (a backup will be created). Proceed?"; then
log "Skipped XDG autostart."
return 0
fi
cp -a "$desktop_file" "${desktop_file}.bak.$(date +%Y%m%d_%H%M%S)" || true
fi
cat >"$desktop_file" <<'SH'
[Desktop Entry]
Type=Application
Name=Autorandr Apply
Comment=Apply autorandr profile at login
Exec=autorandr --change
X-GNOME-Autostart-enabled=true
SH
log "Installed XDG autostart: $desktop_file"
}
install_systemd_user_unit() {
local unit_dir="$HOME/.config/systemd/user"
local unit_file="$unit_dir/autorandr-change.service"
mkdir -p "$unit_dir"
if [[ -f "$unit_file" ]]; then
if ! confirm_yes "This will overwrite $unit_file (a backup will be created). Proceed?"; then
log "Skipped systemd user unit."
return 0
fi
cp -a "$unit_file" "${unit_file}.bak.$(date +%Y%m%d_%H%M%S)" || true
fi
cat >"$unit_file" <<'UNIT'
[Unit]
Description=Apply autorandr profile
After=graphical-session.target
[Service]
Type=oneshot
ExecStart=/usr/bin/autorandr --change
[Install]
WantedBy=default.target
UNIT
systemctl --user daemon-reload
systemctl --user enable autorandr-change.service
log "Installed and enabled systemd user unit: $unit_file"
}
enable_system_services_if_present() {
if systemctl list-unit-files | grep -q '^autorandr\.service'; then
if confirm_yes "Enable system-wide autorandr.service now?"; then
sudo systemctl enable --now autorandr.service || true
fi
fi
if systemctl list-unit-files | grep -q '^autorandr-resume\.service'; then
if confirm_yes "Enable system-wide autorandr-resume.service now?"; then
sudo systemctl enable --now autorandr-resume.service || true
fi
fi
}
try_apply_now() {
if [[ -n "${DISPLAY:-}" ]]; then
log "DISPLAY is set; attempting autorandr --change now..."
if ! autorandr --change; then
err "autorandr --change failed in-session (you can run it manually later)."
return 0
fi
log "autorandr applied."
else
log "DISPLAY not set; skipping immediate autorandr apply."
fi
}
main() {
have_cmd sudo || { err "sudo required"; exit 1; }
sudo -v || { err "sudo auth failed"; exit 1; }
ensure_deps
choose_profile
install_preswitch_hook
if confirm_yes "Enable system-wide autorandr services (if present)?"; then
enable_system_services_if_present
fi
if confirm_yes "Install XDG autostart entry to run 'autorandr --change' at login?"; then
install_xdg_autostart
fi
if confirm_yes "Install a systemd user unit to run 'autorandr --change' at login?"; then
install_systemd_user_unit
fi
try_apply_now
log "Done. Profile selected: $AUTORANDR_PROFILE"
log "Profile config path: ~/.config/autorandr/$AUTORANDR_PROFILE"
}
main "$@"
EOF
}
generate_pwsh_script() {
log "Generating (optional): $PWSH_SCRIPT"
safe_write_file "$PWSH_SCRIPT" 755 <<'EOF'
#!/usr/bin/env pwsh
<###
Pwsh Monitor Setup — Integrated, Optimized Build
TARGET: Linux + X11 (xrandr), PowerShell Core (pwsh)
Highlights:
- Dependency detection + optional auto-install (supports apt/dnf/zypper/pacman).
- X session readiness checks.
- Tkinter overlay verification.
- Interactive mode selection per output, with ordering prompt.
- Saves layout to JSON for reuse.
NOTE: This script is generated by the RK3588 toolkit installer.
###>
[CmdletBinding()]
param(
[switch] $DebugMode,
[switch] $AutoInstall,
[switch] $NoInstall,
[int] $OverlaySeconds = 5,
[string] $ConfigPath
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
if ($DebugMode) { $VerbosePreference = 'Continue' }
function Stop-TranscriptSafe { try { Stop-Transcript | Out-Null } catch {} }
$tsFile = "/tmp/pwsh_monitor_setup-$(Get-Date -Format 'yyyyMMdd-HHmmss').log"
try { Start-Transcript -Path $tsFile -Force | Out-Null } catch {}
function Test-Cmd([string]$Name) { $null -ne (Get-Command -Name $Name -ErrorAction SilentlyContinue) }
function Test-IsRoot { try { ((& id '-u' 2>$null) -eq 0) } catch { $false } }
function Detect-PackageManager {
if (Test-Cmd 'apt-get') { return 'apt' }
if (Test-Cmd 'dnf') { return 'dnf' }
if (Test-Cmd 'zypper') { return 'zypper' }
if (Test-Cmd 'pacman') { return 'pacman' }
return $null
}
function Install-Dep([string]$pkg) {
$pm = Detect-PackageManager
if (-not $pm) { throw 'No supported package manager found.' }
if (-not (Test-IsRoot)) { throw 'Auto-install requires root.' }
switch ($pm) {
'apt' { & apt-get update; & apt-get install -y $pkg }
'dnf' { & dnf install -y $pkg }
'zypper' { & zypper -n install $pkg }
'pacman' { & pacman -Sy --noconfirm $pkg }
}
}
function Ensure-DepMapped([string]$cmd, [hashtable]$pkgMap, [string]$friendly) {
if (Test-Cmd $cmd) { return }
if ($NoInstall) { throw "Missing dependency '$friendly' and -NoInstall specified." }
if (-not $AutoInstall) { throw "Missing dependency '$friendly'. Rerun with -AutoInstall." }
$pm = Detect-PackageManager
if (-not $pkgMap.ContainsKey($pm)) { throw "Unsupported PM for '$friendly'." }
Install-Dep $pkgMap[$pm]
if (-not (Test-Cmd $cmd)) { throw "Dependency '$friendly' missing after install." }
}
function Test-PythonTk {
@'
import sys
try:
import tkinter as t
root=t.Tk(); root.withdraw()
print('OK')
except Exception:
sys.exit(1)
'@ | Set-Content -Path ($tmp = [IO.Path]::GetTempFileName() + '.py') -Encoding UTF8
try {
$p = Start-Process -FilePath 'python3' -ArgumentList @($tmp) -PassThru -NoNewWindow -Wait
return ($p.ExitCode -eq 0)
} finally {
try { Remove-Item $tmp -Force -ErrorAction SilentlyContinue } catch {}
}
}
function Assert-XSessionOrExit {
if (-not $env:DISPLAY) { $env:DISPLAY = ':0' }
for ($i=0; $i -lt 6; $i++) {
try { & xrandr --current | Out-Null; return } catch { Start-Sleep -Milliseconds 300 }
}
Write-Error 'No X11 display/xrandr available.'
Stop-TranscriptSafe
exit 20
}
try {
Ensure-DepMapped 'xrandr' @{ apt='xrandr'; dnf='xrandr'; zypper='xrandr'; pacman='xorg-xrandr' } 'xrandr'
Ensure-DepMapped 'python3' @{ apt='python3'; dnf='python3'; zypper='python3'; pacman='python' } 'python3'
} catch {
Write-Error $_.Exception.Message
Stop-TranscriptSafe
exit 2
}
Assert-XSessionOrExit
if (-not (Test-PythonTk)) {
Write-Error "python3-tk check failed. Install tkinter (python3-tk)."
Stop-TranscriptSafe
exit 2
}
function Get-XrandrLines { (& xrandr --query | Out-String) -split "`n" }
function Get-ConnectedOutputs { Get-XrandrLines | ForEach-Object { if ($_ -match '^\s*(\S+)\s+connected') { $Matches[1] } } }
function Invoke-Xrandr([string[]]$XRArgs) {
$p = Start-Process -FilePath 'xrandr' -ArgumentList $XRArgs -PassThru -NoNewWindow -Wait
if ($DebugMode) { Write-Verbose ("xrandr " + ($XRArgs -join ' ') + " => " + $p.ExitCode) }
$p.ExitCode
}
function Get-OutputModeCandidates([string]$Output) {
$lines = Get-XrandrLines
$in = $false
$cands = New-Object 'System.Collections.Generic.List[object]'
foreach ($ln in $lines) {
if (-not $in) {
if ($ln -match "^\s*$([Regex]::Escape($Output))\s+connected") { $in = $true }
} else {
if ($ln -match '^\S+\s+(connected|disconnected)') { break }
if ($ln -match '^\s+((\d{3,5}x\d{3,5})(i)?\S*)\s+(.+)$') {
$tok=$Matches[1]; $res=$Matches[2]; $rest=$Matches[4]
$w=[int]($res -split 'x')[0]; $h=[int]($res -split 'x')[1]
$mAll = [regex]::Matches($rest, '(\d+(?:\.\d+)?)([*+]{0,2})')
foreach ($m in $mAll) {
$hz=$m.Groups[1].Value
if ([string]::IsNullOrWhiteSpace($hz)) { continue }
$cands.Add([pscustomobject]@{Output=$Output; ModeToken=$tok; W=$w; H=$h; Rate=[double]$hz; Label="$tok@$hz"; Prefer=$m.Groups[2].Value -match '\+'; Current=$m.Groups[2].Value -match '\*'})
}
}
}
}
return $cands | Sort-Object @{e={$_.W*$_.H};d=$true}, @{e={$_.Rate};d=$true}
}
function Show-OverlayTk([int]$W,[int]$H,[int]$X,[int]$Y,[int]$Seconds) {
$py = @'
import sys, tkinter as t
w, h, x, y, sec = map(int, sys.argv[1:6])
root = t.Tk(); root.overrideredirect(1); root.attributes("-topmost", True)
root.geometry(f"{w}x{h}+{x}+{y}")
t.Frame(root, width=w, height=h, highlightbackground="red", highlightthickness=8).pack()
root.after(sec*1000, root.destroy); root.mainloop()
'@
$tmp = [IO.Path]::GetTempFileName() + '.py'
[IO.File]::WriteAllText($tmp, $py)
$p = Start-Process -FilePath 'python3' -ArgumentList @($tmp, $W, $H, $X, $Y, $Seconds) -PassThru -NoNewWindow
try { Wait-Process -Id $p.Id -Timeout ([Math]::Max($Seconds+3, 8)) } catch { try { Stop-Process -Id $p.Id -Force } catch {} }
try { Remove-Item $tmp -Force -ErrorAction SilentlyContinue } catch {}
}
$outs = Get-ConnectedOutputs
if (-not $outs) {
Write-Error 'No connected monitors detected.'
Stop-TranscriptSafe
exit 21
}
$ordered = New-Object 'System.Collections.Generic.List[string]'
if ($outs.Count -gt 1) {
Write-Host "Monitors: $($outs -join ', ')" -ForegroundColor Cyan
$rem = [System.Collections.Generic.List[string]]::new(); $rem.AddRange([string[]]$outs)
while ($rem.Count -gt 0) {
$q = if ($ordered.Count -eq 0) { 'Leftmost monitor?' } else { "Right of $($ordered[-1])?" }
for ($i=0; $i -lt $rem.Count; $i++) { Write-Host " [$($i+1)] $($rem[$i])" }
$ans = Read-Host "$q (1-$($rem.Count))"
if ($ans -match '^\d+$' -and [int]$ans -ge 1 -and [int]$ans -le $rem.Count) {
$idx=[int]$ans-1; $ordered.Add($rem[$idx]); $rem.RemoveAt($idx)
}
}
} else { $ordered.Add($outs[0]) }
$all = @{}; foreach ($o in $ordered) { $all[$o] = Get-OutputModeCandidates $o }
$chosen = [ordered]@{}; foreach ($o in $ordered) { $chosen[$o] = $null }
foreach ($current in $ordered) {
Write-Host "Select mode for $current" -ForegroundColor Cyan
$cands = @($all[$current])
if (-not $cands -or $cands.Count -lt 1) { continue }
$idx=1
foreach ($c in $cands) { Write-Host (" [{0}] {1} ({2}x{3})" -f $idx++, $c.Label, $c.W, $c.H) }
$ans = Read-Host "Choice (1-$($cands.Count)), or Enter to keep current"
if ($ans -match '^\d+$' -and [int]$ans -ge 1 -and [int]$ans -le $cands.Count) {
$c = $cands[[int]$ans-1]
$x=0
foreach ($o in $ordered) {
$tgt = if ($o -eq $current) { $c } else { $chosen[$o] ?? $all[$o][0] }
if ($tgt) {
$args = @('--output', $o, '--mode', $tgt.ModeToken, '--pos', "${x}x0")
if ($tgt.Rate) { $args += @('--rate', ("{0:0.##}" -f $tgt.Rate)) }
Invoke-Xrandr $args | Out-Null
$x += $tgt.W
}
}
Show-OverlayTk $c.W $c.H 0 0 $OverlaySeconds
if ((Read-Host 'Keep? (y/n)') -eq 'y') { $chosen[$current] = $c }
}
}
$persist = [ordered]@{}
foreach ($o in $ordered) {
$c = $chosen[$o]
if ($null -eq $c) { continue }
$persist[$o] = [ordered]@{ mode=$c.ModeToken; rate=$(if($c.Rate){[double]$c.Rate}else{$null}) }
}
$target = $ConfigPath
if ([string]::IsNullOrWhiteSpace($target)) { $target = "$HOME/.config/pwsh-monitor-layout.json" }
$dir = Split-Path -Parent $target
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }
($persist | ConvertTo-Json) | Set-Content -Path $target
Write-Host "Saved layout to: $target" -ForegroundColor Green
Stop-TranscriptSafe
exit 0
EOF
}
generate_toolkit_scripts() {
log "=== Step 2/4: Generating toolkit scripts under: $TOOLKIT_DIR ==="
mkdir -p "$BIN_DIR" "$LOG_DIR" "$STATE_DIR"
generate_audit_script
generate_menu_script
generate_interactive_script
generate_dual_fix_script
generate_bootstrap_script
generate_pwsh_script
log "Toolkit scripts generated successfully."
}
# -----------------------------
# Step 3: Self-tests (syntax / presence)
# -----------------------------
run_self_tests() {
log "=== Step 3/4: Running basic self-tests (syntax checks) ==="
local failed=0
for f in "$AUDIT_SCRIPT" "$MENU_SCRIPT" "$INTERACTIVE_SCRIPT" "$DUAL_FIX_SCRIPT" "$BOOTSTRAP_SCRIPT"; do
if [[ -f "$f" ]]; then
if bash -n "$f"; then
log "OK: bash -n $(basename "$f")"
else
err "FAIL: bash -n $(basename "$f")"
failed=1
fi
else
err "Missing generated file: $f"
failed=1
fi
done
if [[ -f "$PWSH_SCRIPT" ]]; then
if head -n 1 "$PWSH_SCRIPT" | grep -q 'pwsh'; then
log "OK: pwsh script present (pwsh not validated here): $(basename "$PWSH_SCRIPT")"
else
err "WARN: pwsh script shebang unexpected: $PWSH_SCRIPT"
fi
fi
if [[ "$failed" -ne 0 ]]; then
err "One or more self-tests failed. Review logs above."
return 1
fi
log "Self-tests passed."
}
# -----------------------------
# Step 4: Final guidance
# -----------------------------
print_next_steps() {
log "=== Step 4/4: Next steps ==="
cat <<EOF
Toolkit location:
$TOOLKIT_DIR
Generated scripts:
1) Capability audit (safe; optional prompts for tunings):
$AUDIT_SCRIPT
2) Menu-driven display setup wrapper:
$MENU_SCRIPT
3) Interactive mode picker (REQUIRES X SESSION; CHANGES DISPLAY MODES):
$INTERACTIVE_SCRIPT
4) RK3588 dual-monitor fix (REQUIRES X SESSION; CHANGES DISPLAY MODES):
$DUAL_FIX_SCRIPT
5) Autorandr bootstrap (optional autostart/systemd integration):
$BOOTSTRAP_SCRIPT
6) Optional PowerShell (pwsh) interactive configurator (requires pwsh installed):
$PWSH_SCRIPT
Recommended flow:
# Optional: baseline audit
"$AUDIT_SCRIPT"
# Install display tools / collect info / create custom xrandr / save autorandr profile
"$MENU_SCRIPT"
# In X session: safe interactive mode picking
"$INTERACTIVE_SCRIPT"
# If you have the specific HDMI+DP low-mode issue
"$DUAL_FIX_SCRIPT"
# Wire autorandr persistence at login/resume
"$BOOTSTRAP_SCRIPT"
EOF
}
# -----------------------------
# Main
# -----------------------------
main() {
log "RK3588 Display Toolkit Installer — ${VERSION}"
require_home_context
check_platform
install_dependencies
generate_toolkit_scripts
run_self_tests
print_next_steps
log "Setup complete."
}
main "$@"
#Recommended flow:
#Optional: baseline audit
/home/linaro/rk3588-display-toolkit/bin/rk3588_audit.sh
#Install display tools / collect info / create custom xrandr / save autorandr profile
bash /home/linaro/rk3588-display-toolkit/bin/debian_display_setup.sh
#In X session: safe interactive mode picking
bash /home/linaro/rk3588-display-toolkit/bin/monitor_setup_interactive.sh
pwsh /home/linaro/rk3588-display-toolkit/bin/pwsh_monitor_setup.ps1
#If you have the specific HDMI+DP low-mode issue
bash /home/linaro/rk3588-display-toolkit/bin/rk3588_dual_display_setup.sh
#Wire autorandr persistence at login/resume
bash /home/linaro/rk3588-display-toolkit/bin/rk3588_autorandr_bootstrap.sh
#/home/linaro/rk3588-display-toolkit/bin/debian_display_setup.sh
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# debian_display_setup.sh
# Menu-driven helper for:
# - One-time package install
# - Monitor info collection (xrandr + sysfs EDID + optional root-only tools)
# - Custom xrandr script template
# - Autorandr profile save
MONITOR_INFO_OUTDIR=""
CUSTOM_XRANDR_SCRIPT_PATH="${HOME}/my_custom_display_config.sh"
TOOLKIT_HOME="${HOME}/rk3588-display-toolkit"
CONFIG_DIR="${TOOLKIT_HOME}/state/debian_display_master"
PREP_DONE_FLAG="${CONFIG_DIR}/system_prep_done.flag"
log() { printf '[%(%F %T)T] %s\n' -1 "$*"; }
ensure_config_dir() {
mkdir -p "$CONFIG_DIR"
if [[ $EUID -eq 0 && -n "${SUDO_USER:-}" && "${SUDO_USER}" != "root" ]]; then
chown -R "$SUDO_USER:$(id -gn "$SUDO_USER")" "$CONFIG_DIR" 2>/dev/null || true
fi
}
require_root_or_return() {
if [[ $EUID -ne 0 ]]; then
echo "ERROR: This step must be run with sudo/root." >&2
echo "Re-run with: sudo bash $0" >&2
return 1
fi
return 0
}
system_prep() {
echo
echo "--- Stage 1: One-Time System Preparation (sudo required) ---"
require_root_or_return || return 1
echo ">>> Updating apt lists..."
apt-get update
echo ">>> Installing autorandr + tools..."
apt-get install -y --no-install-recommends \
autorandr xrandr x11-xserver-utils python3 python3-tk \
read-edid ddcutil hwinfo inxi lshw edid-decode bc
if systemctl list-unit-files | grep -q '^autorandr\.service'; then
systemctl enable --now autorandr.service || true
fi
if systemctl list-unit-files | grep -q '^autorandr-resume\.service'; then
systemctl enable --now autorandr-resume.service || true
fi
ensure_config_dir
date > "$PREP_DONE_FLAG"
echo ">>> System preparation complete."
}
_mi_setup_outdir() {
local real_user home_base
if [[ $EUID -eq 0 && -n "${SUDO_USER:-}" && "$SUDO_USER" != "root" ]]; then
real_user="$SUDO_USER"
else
real_user="$(whoami)"
fi
home_base="$(getent passwd "$real_user" | cut -d: -f6)"
[[ -n "$home_base" && -d "$home_base" ]] || home_base="/tmp"
MONITOR_INFO_OUTDIR="${home_base}/monitor-info-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$MONITOR_INFO_OUTDIR"
: >"$MONITOR_INFO_OUTDIR/summary.txt"
if [[ $EUID -eq 0 && -n "${SUDO_USER:-}" ]]; then
chown -R "$SUDO_USER:$(id -gn "$SUDO_USER")" "$MONITOR_INFO_OUTDIR" 2>/dev/null || true
fi
}
_mi_log_cmd() {
local logfile="$1"; shift
{
echo "===== $(date '+%F %T') : $* ====="
"$@" 2>&1 || echo "(ERROR: '$*' failed code $?)"
echo
} >>"$MONITOR_INFO_OUTDIR/$logfile"
}
_mi_detect_connectors_sysfs() {
local regex='^[a-zA-Z]+(-[a-zA-Z0-9]+)*-[0-9]+$'
echo "--- DRM connectors (sysfs) ---" >>"$MONITOR_INFO_OUTDIR/summary.txt"
for card in /sys/class/drm/card*; do
[[ -d "$card" ]] || continue
for p in "$card"/*; do
[[ -d "$p" ]] || continue
local name
name="$(basename "$p")"
if [[ "$name" =~ $regex ]]; then
echo "DRM connector: $name" >>"$MONITOR_INFO_OUTDIR/summary.txt"
fi
done
done
}
_mi_collect_sysfs_edid() {
command -v edid-decode >/dev/null 2>&1 || return 0
echo "--- EDID decode (sysfs) ---" >>"$MONITOR_INFO_OUTDIR/summary.txt"
while IFS= read -r edid; do
[[ -s "$edid" ]] || continue
_mi_log_cmd "edid_sysfs.log" edid-decode "$edid"
done < <(find /sys/class/drm -name edid 2>/dev/null || true)
}
_mi_generate_cvt_interactive() {
if ! command -v cvt >/dev/null 2>&1; then
echo "Note: 'cvt' not found; skipping modeline generator." >>"$MONITOR_INFO_OUTDIR/summary.txt"
return 0
fi
echo
read -r -p "Generate a modeline via cvt? Enter 'width height refresh' (or Enter to skip): " W H R
if [[ -n "${W:-}" && -n "${H:-}" && -n "${R:-}" ]]; then
cvt "$W" "$H" "$R" | tee -a "$MONITOR_INFO_OUTDIR/cvt_modeline.log" >/dev/null
echo "Wrote: $MONITOR_INFO_OUTDIR/cvt_modeline.log"
fi
}
monitor_info() {
echo
echo "--- Stage 2: Collect Monitor Information ---"
_mi_setup_outdir
echo "Collecting monitor information into: $MONITOR_INFO_OUTDIR"
_mi_detect_connectors_sysfs
if command -v xrandr >/dev/null 2>&1; then
_mi_log_cmd "xrandr_verbose.log" xrandr --verbose
xrandr | tee -a "$MONITOR_INFO_OUTDIR/summary.txt" >/dev/null || true
else
echo "xrandr not found. Run Stage 1 first." | tee -a "$MONITOR_INFO_OUTDIR/summary.txt" >/dev/null
fi
_mi_collect_sysfs_edid
if [[ $EUID -eq 0 ]]; then
command -v ddcutil >/dev/null 2>&1 && _mi_log_cmd "ddcutil_detect.log" ddcutil detect --verbose || true
command -v get-edid >/dev/null 2>&1 && _mi_log_cmd "get_edid.log" get-edid || true
command -v hwinfo >/dev/null 2>&1 && _mi_log_cmd "hwinfo_monitor.log" hwinfo --monitor || true
command -v lshw >/dev/null 2>&1 && _mi_log_cmd "lshw_display.log" lshw -C display || true
else
echo "Note: root-only tools (ddcutil, get-edid, hwinfo, lshw) are skipped unless run with sudo." | tee -a "$MONITOR_INFO_OUTDIR/summary.txt" >/dev/null
fi
_mi_generate_cvt_interactive
echo "Done. Summary: $MONITOR_INFO_OUTDIR/summary.txt"
}
create_or_edit_custom_xrandr() {
echo
echo "--- Stage 3: Create/Edit Custom Xrandr Configuration Script ---"
if [[ ! -f "$CUSTOM_XRANDR_SCRIPT_PATH" ]]; then
cat >"$CUSTOM_XRANDR_SCRIPT_PATH" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
# Custom Xrandr Configuration Script
# Edit this file to match your connector names and desired modes.
command -v xrandr >/dev/null 2>&1 || { echo "Error: xrandr not found" >&2; exit 1; }
echo "Applying custom xrandr settings..."
# Example:
# OUTPUT_1_NAME="HDMI-1"
# MODE_1_NAME="1920x1080"
# xrandr --output "$OUTPUT_1_NAME" --mode "$MODE_1_NAME" --primary
# Safe fallback:
xrandr --auto
SH
chmod +x "$CUSTOM_XRANDR_SCRIPT_PATH"
echo "Created template: $CUSTOM_XRANDR_SCRIPT_PATH"
else
echo "Custom script exists: $CUSTOM_XRANDR_SCRIPT_PATH"
fi
read -r -p "Open with nano now? (y/N): " ans
if [[ "$ans" =~ ^[Yy]$ ]]; then
nano "$CUSTOM_XRANDR_SCRIPT_PATH"
fi
}
test_custom_xrandr() {
echo
echo "--- Stage 4: Test Custom Xrandr Script (must be in X session) ---"
if [[ -z "${DISPLAY:-}" ]]; then
echo "ERROR: DISPLAY is not set. Run from an active X session." >&2
return 1
fi
if [[ $EUID -eq 0 ]]; then
echo "WARNING: Running xrandr as root is not recommended. Prefer running as your user." >&2
fi
if ! bash "$CUSTOM_XRANDR_SCRIPT_PATH"; then
echo "Error: custom xrandr script failed." >&2
echo "Recovery hint: switch to a TTY (Ctrl+Alt+F2), then try: DISPLAY=:0 xrandr --auto (if possible), or reboot." >&2
return 1
fi
echo "Custom xrandr script executed."
}
save_autorandr_profile() {
echo
echo "--- Stage 5: Save Autorandr Profile (run in X session) ---"
command -v autorandr >/dev/null 2>&1 || { echo "Error: autorandr not found. Run Stage 1." >&2; return 1; }
read -r -p "Enter profile name: " pname
pname="$(echo "$pname" | tr -s ' /\:&?' '_')"
[[ -n "$pname" ]] || { echo "No profile name provided." >&2; return 1; }
autorandr --save "$pname"
echo "Saved autorandr profile: $pname"
}
main_menu() {
ensure_config_dir
echo
echo "Debian Bullseye Display Setup & Management Utility"
echo "=================================================="
echo "NOTE: Some actions require sudo, others require an active X session."
PS3="Choose an option: "
local options=(
"1) One-time system preparation (sudo required)"
"2) Collect monitor information"
"3) Create/edit custom xrandr script"
"4) Test custom xrandr script (X session)"
"5) Save autorandr profile (X session)"
"6) Exit"
)
select opt in "${options[@]}"; do
case "$REPLY" in
1) system_prep ;;
2) monitor_info ;;
3) create_or_edit_custom_xrandr ;;
4) test_custom_xrandr ;;
5) save_autorandr_profile ;;
6) echo "Exiting."; exit 0 ;;
*) echo "Invalid option." ;;
esac
echo
read -r -p "Press Enter to return to the menu..." _
done
}
main_menu
#/home/linaro/rk3588-display-toolkit/bin/monitor_setup_interactive.sh
#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'
# monitor_setup_interactive.sh
#
# Safer interactive mode picker with visual verification:
# - SafeMode-first: tries a short list of common resolutions first
# - Output ordering: HDMI/DP/DVI/eDP/VGA then the rest
# - Tk overlay: draws a red border on the active monitor
#
# WARNING: This script changes display modes.
# Recovery: switch to a TTY (Ctrl+Alt+F2) and run: DISPLAY=:0 xrandr --auto (if possible) or reboot.
LOGF="${LOGF:-/tmp/monitor-setup.log}"
exec > >(tee -a "$LOGF") 2>&1
log() { printf '[%(%F %T)T] %s\n' -1 "$*"; }
fail() { log "ERROR: $*"; exit 1; }
need() { command -v "$1" >/dev/null 2>&1 || fail "$1 not found"; }
need xrandr
need python3
need timeout
python3 - <<'PY' 2>/dev/null || fail "python3-tk not available. Install python3-tk."
import tkinter
PY
[[ -n "${DISPLAY:-}" ]] || fail "DISPLAY is not set. Run from an active X session."
SAFEMODES=(
1920x1080 1680x1050 1600x900 1440x900 1366x768 1280x1024 1280x960
1280x800 1280x720 1024x768 800x600 640x480
)
get_outputs() { xrandr --query | awk '/ connected/{print $1}'; }
sort_outputs() {
local -a outs
mapfile -t outs < <(get_outputs)
local prefs=(HDMI DP DVI eDP VGA)
for p in "${prefs[@]}"; do
for o in "${outs[@]}"; do [[ "$o" == ${p}* ]] && echo "$o"; done
done
for o in "${outs[@]}"; do
local hit=0
for p in "${prefs[@]}"; do [[ "$o" == ${p}* ]] && hit=1; done
((hit==0)) && echo "$o"
done
}
get_modes_for_output() {
local out="$1"
xrandr --query | awk -v o="$out" '
$1==o {inblock=1; next}
/^[A-Z]/ {inblock=0}
inblock && $1 ~ /^[0-9]+x[0-9]+/ {print $1}
' | awk '!seen[$0]++'
}
current_mode_for_output() {
local out="$1"
xrandr --query | awk -v o="$out" '
$1==o {inblock=1; next}
/^[A-Z]/ {inblock=0}
inblock && $1 ~ /^[0-9]+x[0-9]+/ {
if ($0 ~ /\*/) {print $1; exit}
}
'
}
pick_safelist() {
local -a avail=()
mapfile -t avail
local -a picked=()
for safe in "${SAFEMODES[@]}"; do
for m in "${avail[@]}"; do
[[ "$m" == "$safe" ]] && { picked+=("$m"); break; }
done
(( ${#picked[@]} >= 4 )) && break
done
if (( ${#picked[@]} > 0 )); then
printf '%s\n' "${picked[@]}"
else
printf '%s\n' "${avail[@]}" | head -n 4
fi
}
current_geometry() {
local out="$1"
xrandr --query | awk -v o="$out" '
$1==o { if (match($0, /[0-9]+x[0-9]+\+[0-9]+\+[0-9]+/)) { print substr($0, RSTART, RLENGTH) } }
'
}
show_rectangle() {
local W="$1" H="$2" X="$3" Y="$4"
log " overlay ${W}x${H}+${X}+${Y} for 5s"
timeout 7s python3 - "$W" "$H" "$X" "$Y" <<'PY'
import sys, tkinter as t
W,H,X,Y = map(int, sys.argv[1:])
root = t.Tk()
root.overrideredirect(True)
root.geometry(f"{W}x{H}+{X}+{Y}")
frame = t.Frame(root, width=W, height=H, highlightbackground='red', highlightthickness=8)
frame.pack()
root.attributes('-topmost', True)
root.after(5000, root.quit)
root.mainloop()
try:
root.destroy()
except Exception:
pass
PY
}
ask_ynq() {
local prompt="$1" ch
while true; do
printf '%s' "$prompt" > /dev/tty
IFS= read -r -n1 ch < /dev/tty || ch=""
printf '\n' > /dev/tty
case "$ch" in
y|Y) return 0;;
n|N) return 1;;
q|Q) return 2;;
*) prompt="Please type y (accept), n (next), or q (quit): ";;
esac
done
}
restore_mode_best_effort() {
local out="$1" prev="$2"
if [[ -n "$prev" ]]; then
xrandr --output "$out" --mode "$prev" >/dev/null 2>&1 || true
else
xrandr --output "$out" --auto >/dev/null 2>&1 || true
fi
}
try_modes_for_output() {
local out="$1"; shift
local -a modes=("$@")
local prev
prev="$(current_mode_for_output "$out" || true)"
for mode in "${modes[@]}"; do
log "Trying $out -> $mode"
if ! xrandr --output "$out" --mode "$mode"; then
log "Failed to set $mode on $out"
continue
fi
sleep 0.4
local geom
geom="$(current_geometry "$out" || true)"
if [[ "$geom" =~ ([0-9]+)x([0-9]+)\+([0-9]+)\+([0-9]+) ]]; then
show_rectangle "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}" "${BASH_REMATCH[4]}"
if ask_ynq "Keep $mode for $out? [y/n/q] "; then
log "Accepted $mode for $out"
return 0
else
local rc=$?
if ((rc==2)); then
restore_mode_best_effort "$out" "$prev"
return 2
fi
fi
else
log "Could not parse geometry for $out after applying $mode (continuing)."
fi
done
restore_mode_best_effort "$out" "$prev"
return 1
}
log "Starting interactive monitor configuration..."
mapfile -t outputs < <(sort_outputs)
(( ${#outputs[@]} > 0 )) || fail "No connected outputs detected by xrandr."
for out in "${outputs[@]}"; do
log "Configuring $out..."
mapfile -t avail < <(get_modes_for_output "$out")
(( ${#avail[@]} > 0 )) || { log "No modes found for $out (skipping)."; continue; }
mapfile -t testlist < <(printf '%s\n' "${avail[@]}" | pick_safelist)
if try_modes_for_output "$out" "${testlist[@]}"; then
continue
else
rc=$?
((rc==2)) && { log "Quit requested."; exit 0; }
fi
if ask_ynq "No SafeMode accepted for $out. Try ALL modes (riskier)? [y/n/q] "; then
if try_modes_for_output "$out" "${avail[@]}"; then
continue
fi
else
rc=$?
((rc==2)) && { log "Quit requested."; exit 0; }
fi
done
log "Done. For persistence, save an autorandr profile using debian_display_setup.sh."
#/home/linaro/rk3588-display-toolkit/bin/pwsh_monitor_setup.ps1
#!/usr/bin/env pwsh
<###
Pwsh Monitor Setup — Integrated, Optimized Build
TARGET: Linux + X11 (xrandr), PowerShell Core (pwsh)
Highlights:
- Dependency detection + optional auto-install (supports apt/dnf/zypper/pacman).
- X session readiness checks.
- Tkinter overlay verification.
- Interactive mode selection per output, with ordering prompt.
- Saves layout to JSON for reuse.
NOTE: This script is generated by the RK3588 toolkit installer.
###>
[CmdletBinding()]
param(
[switch] $DebugMode,
[switch] $AutoInstall,
[switch] $NoInstall,
[int] $OverlaySeconds = 5,
[string] $ConfigPath
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
if ($DebugMode) { $VerbosePreference = 'Continue' }
function Stop-TranscriptSafe { try { Stop-Transcript | Out-Null } catch {} }
$tsFile = "/tmp/pwsh_monitor_setup-$(Get-Date -Format 'yyyyMMdd-HHmmss').log"
try { Start-Transcript -Path $tsFile -Force | Out-Null } catch {}
function Test-Cmd([string]$Name) { $null -ne (Get-Command -Name $Name -ErrorAction SilentlyContinue) }
function Test-IsRoot { try { ((& id '-u' 2>$null) -eq 0) } catch { $false } }
function Detect-PackageManager {
if (Test-Cmd 'apt-get') { return 'apt' }
if (Test-Cmd 'dnf') { return 'dnf' }
if (Test-Cmd 'zypper') { return 'zypper' }
if (Test-Cmd 'pacman') { return 'pacman' }
return $null
}
function Install-Dep([string]$pkg) {
$pm = Detect-PackageManager
if (-not $pm) { throw 'No supported package manager found.' }
if (-not (Test-IsRoot)) { throw 'Auto-install requires root.' }
switch ($pm) {
'apt' { & apt-get update; & apt-get install -y $pkg }
'dnf' { & dnf install -y $pkg }
'zypper' { & zypper -n install $pkg }
'pacman' { & pacman -Sy --noconfirm $pkg }
}
}
function Ensure-DepMapped([string]$cmd, [hashtable]$pkgMap, [string]$friendly) {
if (Test-Cmd $cmd) { return }
if ($NoInstall) { throw "Missing dependency '$friendly' and -NoInstall specified." }
if (-not $AutoInstall) { throw "Missing dependency '$friendly'. Rerun with -AutoInstall." }
$pm = Detect-PackageManager
if (-not $pkgMap.ContainsKey($pm)) { throw "Unsupported PM for '$friendly'." }
Install-Dep $pkgMap[$pm]
if (-not (Test-Cmd $cmd)) { throw "Dependency '$friendly' missing after install." }
}
function Test-PythonTk {
@'
import sys
try:
import tkinter as t
root=t.Tk(); root.withdraw()
print('OK')
except Exception:
sys.exit(1)
'@ | Set-Content -Path ($tmp = [IO.Path]::GetTempFileName() + '.py') -Encoding UTF8
try {
$p = Start-Process -FilePath 'python3' -ArgumentList @($tmp) -PassThru -NoNewWindow -Wait
return ($p.ExitCode -eq 0)
} finally {
try { Remove-Item $tmp -Force -ErrorAction SilentlyContinue } catch {}
}
}
function Assert-XSessionOrExit {
if (-not $env:DISPLAY) { $env:DISPLAY = ':0' }
for ($i=0; $i -lt 6; $i++) {
try { & xrandr --current | Out-Null; return } catch { Start-Sleep -Milliseconds 300 }
}
Write-Error 'No X11 display/xrandr available.'
Stop-TranscriptSafe
exit 20
}
try {
Ensure-DepMapped 'xrandr' @{ apt='xrandr'; dnf='xrandr'; zypper='xrandr'; pacman='xorg-xrandr' } 'xrandr'
Ensure-DepMapped 'python3' @{ apt='python3'; dnf='python3'; zypper='python3'; pacman='python' } 'python3'
} catch {
Write-Error $_.Exception.Message
Stop-TranscriptSafe
exit 2
}
Assert-XSessionOrExit
if (-not (Test-PythonTk)) {
Write-Error "python3-tk check failed. Install tkinter (python3-tk)."
Stop-TranscriptSafe
exit 2
}
function Get-XrandrLines { (& xrandr --query | Out-String) -split "`n" }
function Get-ConnectedOutputs { Get-XrandrLines | ForEach-Object { if ($_ -match '^\s*(\S+)\s+connected') { $Matches[1] } } }
function Invoke-Xrandr([string[]]$XRArgs) {
$p = Start-Process -FilePath 'xrandr' -ArgumentList $XRArgs -PassThru -NoNewWindow -Wait
if ($DebugMode) { Write-Verbose ("xrandr " + ($XRArgs -join ' ') + " => " + $p.ExitCode) }
$p.ExitCode
}
function Get-OutputModeCandidates([string]$Output) {
$lines = Get-XrandrLines
$in = $false
$cands = New-Object 'System.Collections.Generic.List[object]'
foreach ($ln in $lines) {
if (-not $in) {
if ($ln -match "^\s*$([Regex]::Escape($Output))\s+connected") { $in = $true }
} else {
if ($ln -match '^\S+\s+(connected|disconnected)') { break }
if ($ln -match '^\s+((\d{3,5}x\d{3,5})(i)?\S*)\s+(.+)$') {
$tok=$Matches[1]; $res=$Matches[2]; $rest=$Matches[4]
$w=[int]($res -split 'x')[0]; $h=[int]($res -split 'x')[1]
$mAll = [regex]::Matches($rest, '(\d+(?:\.\d+)?)([*+]{0,2})')
foreach ($m in $mAll) {
$hz=$m.Groups[1].Value
if ([string]::IsNullOrWhiteSpace($hz)) { continue }
$cands.Add([pscustomobject]@{Output=$Output; ModeToken=$tok; W=$w; H=$h; Rate=[double]$hz; Label="$tok@$hz"; Prefer=$m.Groups[2].Value -match '\+'; Current=$m.Groups[2].Value -match '\*'})
}
}
}
}
return $cands | Sort-Object @{e={$_.W*$_.H};d=$true}, @{e={$_.Rate};d=$true}
}
function Show-OverlayTk([int]$W,[int]$H,[int]$X,[int]$Y,[int]$Seconds) {
$py = @'
import sys, tkinter as t
w, h, x, y, sec = map(int, sys.argv[1:6])
root = t.Tk(); root.overrideredirect(1); root.attributes("-topmost", True)
root.geometry(f"{w}x{h}+{x}+{y}")
t.Frame(root, width=w, height=h, highlightbackground="red", highlightthickness=8).pack()
root.after(sec*1000, root.destroy); root.mainloop()
'@
$tmp = [IO.Path]::GetTempFileName() + '.py'
[IO.File]::WriteAllText($tmp, $py)
$p = Start-Process -FilePath 'python3' -ArgumentList @($tmp, $W, $H, $X, $Y, $Seconds) -PassThru -NoNewWindow
try { Wait-Process -Id $p.Id -Timeout ([Math]::Max($Seconds+3, 8)) } catch { try { Stop-Process -Id $p.Id -Force } catch {} }
try { Remove-Item $tmp -Force -ErrorAction SilentlyContinue } catch {}
}
$outs = Get-ConnectedOutputs
if (-not $outs) {
Write-Error 'No connected monitors detected.'
Stop-TranscriptSafe
exit 21
}
$ordered = New-Object 'System.Collections.Generic.List[string]'
if ($outs.Count -gt 1) {
Write-Host "Monitors: $($outs -join ', ')" -ForegroundColor Cyan
$rem = [System.Collections.Generic.List[string]]::new(); $rem.AddRange([string[]]$outs)
while ($rem.Count -gt 0) {
$q = if ($ordered.Count -eq 0) { 'Leftmost monitor?' } else { "Right of $($ordered[-1])?" }
for ($i=0; $i -lt $rem.Count; $i++) { Write-Host " [$($i+1)] $($rem[$i])" }
$ans = Read-Host "$q (1-$($rem.Count))"
if ($ans -match '^\d+$' -and [int]$ans -ge 1 -and [int]$ans -le $rem.Count) {
$idx=[int]$ans-1; $ordered.Add($rem[$idx]); $rem.RemoveAt($idx)
}
}
} else { $ordered.Add($outs[0]) }
$all = @{}; foreach ($o in $ordered) { $all[$o] = Get-OutputModeCandidates $o }
$chosen = [ordered]@{}; foreach ($o in $ordered) { $chosen[$o] = $null }
foreach ($current in $ordered) {
Write-Host "Select mode for $current" -ForegroundColor Cyan
$cands = @($all[$current])
if (-not $cands -or $cands.Count -lt 1) { continue }
$idx=1
foreach ($c in $cands) { Write-Host (" [{0}] {1} ({2}x{3})" -f $idx++, $c.Label, $c.W, $c.H) }
$ans = Read-Host "Choice (1-$($cands.Count)), or Enter to keep current"
if ($ans -match '^\d+$' -and [int]$ans -ge 1 -and [int]$ans -le $cands.Count) {
$c = $cands[[int]$ans-1]
$x=0
foreach ($o in $ordered) {
$tgt = if ($o -eq $current) { $c } else { $chosen[$o] ?? $all[$o][0] }
if ($tgt) {
$args = @('--output', $o, '--mode', $tgt.ModeToken, '--pos', "${x}x0")
if ($tgt.Rate) { $args += @('--rate', ("{0:0.##}" -f $tgt.Rate)) }
Invoke-Xrandr $args | Out-Null
$x += $tgt.W
}
}
Show-OverlayTk $c.W $c.H 0 0 $OverlaySeconds
if ((Read-Host 'Keep? (y/n)') -eq 'y') { $chosen[$current] = $c }
}
}
$persist = [ordered]@{}
foreach ($o in $ordered) {
$c = $chosen[$o]
if ($null -eq $c) { continue }
$persist[$o] = [ordered]@{ mode=$c.ModeToken; rate=$(if($c.Rate){[double]$c.Rate}else{$null}) }
}
$target = $ConfigPath
if ([string]::IsNullOrWhiteSpace($target)) { $target = "$HOME/.config/pwsh-monitor-layout.json" }
$dir = Split-Path -Parent $target
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }
($persist | ConvertTo-Json) | Set-Content -Path $target
Write-Host "Saved layout to: $target" -ForegroundColor Green
Stop-TranscriptSafe
exit 0
#/home/linaro/rk3588-display-toolkit/bin/rk3588_audit.sh
#!/usr/bin/env bash
set -euo pipefail
set -o errtrace
IFS=$'\n\t'
trap 'echo "Error on or near line ${LINENO}; command exited with status $?" >&2' ERR
# rk3588_audit.sh
#
# Purpose:
# - Create a timestamped audit directory under ~/rk3588_audit_<timestamp>/
# - Collect system/display/GPU/VPU/network/storage snapshots
# - Install diagnostic packages idempotently (skips missing packages)
# - Avoid destructive actions; optional actions require explicit confirmation
START_TS="$(date +%Y%m%d_%H%M%S)"
AUDIT_DIR="$HOME/rk3588_audit_${START_TS}"
LOG_DIR="$AUDIT_DIR/logs"
OUT_DIR="$AUDIT_DIR/out"
REPORT_MD="$AUDIT_DIR/REPORT.md"
LOG_FILE="$LOG_DIR/audit.log"
ENV_FILE="$LOG_DIR/env.txt"
DMESG_FILE="$LOG_DIR/dmesg.txt"
CMD_FAIL_HINT="Check ${LOG_FILE} and ${REPORT_MD} for details."
: "${NET_TIMEOUT:=5}"
: "${FIO_SIZE_MB:=512}"
: "${FIO_BS:=1M}"
: "${FIO_IODEPTH:=16}"
: "${RUN_COLORS:=1}"
TMPDIR_CREATED=""
cleanup() {
if [[ -n "${TMPDIR_CREATED:-}" && -d "$TMPDIR_CREATED" ]]; then
rm -rf "$TMPDIR_CREATED" || true
fi
}
trap cleanup EXIT SIGINT SIGTERM
if [[ -t 1 && "$RUN_COLORS" -eq 1 ]] && command -v tput >/dev/null 2>&1; then
GREEN="$(tput setaf 2)"; YELLOW="$(tput setaf 3)"; RED="$(tput setaf 1)"; BLUE="$(tput setaf 4)"; BOLD="$(tput bold)"; RESET="$(tput sgr0)"
else
GREEN=""; YELLOW=""; RED=""; BLUE=""; BOLD=""; RESET=""
fi
log() { echo -e "$*" | tee -a "$LOG_FILE"; }
section() { log "\n${BOLD}${BLUE}==> $1${RESET}"; }
ensure_dir() { [[ -d "$1" ]] || mkdir -p "$1"; }
ask_yes() {
local prompt="$1" ans
echo
read -r -p "${YELLOW}${prompt}${RESET} (type 'yes' to continue, anything else to cancel): " ans
[[ "$ans" == "yes" ]] || { echo "Cancelled by user."; return 1; }
}
need_sudo() {
command -v sudo >/dev/null 2>&1 || { echo "Error: sudo required." >&2; exit 1; }
sudo -v || { echo "Error: sudo auth failed." >&2; exit 1; }
}
package_available() {
local pkg="$1"
apt-cache policy "$pkg" 2>/dev/null | awk '/Candidate:/ {print $2}' | grep -vq "(none)"
}
is_installed() { dpkg -s "$1" >/dev/null 2>&1; }
ensure_package() {
local pkg="$1"
if is_installed "$pkg"; then
log "Package already installed: ${GREEN}${pkg}${RESET}"
return 0
fi
if ! package_available "$pkg"; then
log "Package not available (skipping): ${YELLOW}${pkg}${RESET}"
return 0
fi
log "Installing package: ${GREEN}${pkg}${RESET}"
sudo apt-get install -y --no-install-recommends "$pkg" >>"$LOG_FILE" 2>&1 || {
echo "Error: Failed to install '$pkg'. ${CMD_FAIL_HINT}" >&2
exit 1
}
}
ensure_packages() { for pkg in "$@"; do ensure_package "$pkg"; done; }
run_continue() {
local title="$1"; shift
log "\n--- $title ---"
"$@" >>"$LOG_FILE" 2>&1 || log " (Command failed but continuing): $*"
}
run_fail() {
local title="$1"; shift
log "\n>>> $title"
"$@" >>"$LOG_FILE" 2>&1 || { echo "Error: $title failed. ${CMD_FAIL_HINT}" >&2; exit 1; }
}
quick_net_check() {
section "Quick network check"
if command -v ping >/dev/null 2>&1 && ping -c1 -W "$NET_TIMEOUT" deb.debian.org >/dev/null 2>&1; then
log "Network reachable."
return 0
fi
log "Network check failed; installs/downloads may be limited."
return 1
}
check_platform() {
section "Platform checks"
local debver arch
debver="$(cut -d'.' -f1 < /etc/debian_version 2>/dev/null || echo unknown)"
arch="$(uname -m 2>/dev/null || echo unknown)"
log "Debian major: $debver"
log "Arch : $arch"
[[ "$debver" == "11" ]] || log "WARNING: tuned for Debian 11 (Bullseye)."
[[ "$arch" == "aarch64" || "$arch" == "arm64" ]] || log "WARNING: tuned for ARM64."
}
enable_nonfree_optional() {
section "Optional: Enable contrib/non-free (Bullseye)"
if ! ask_yes "Enable 'contrib non-free' in /etc/apt/sources.list (backup + apt update)?"; then
log "Skipped enabling contrib/non-free."
return 0
fi
local src="/etc/apt/sources.list"
if [[ ! -f "$src" ]]; then
log "No $src found; skipping."
return 0
fi
sudo cp -a "$src" "${src}.bak.${START_TS}"
TMPDIR_CREATED="$(mktemp -d)"
local tmp="$TMPDIR_CREATED/sources.list"
sudo awk '{
if ($1=="deb" || $1=="deb-src") {
line=$0
has_contrib=match(line,/(^| )contrib( |$)/)
has_nonfree=match(line,/(^| )non-free( |$)/)
if (!has_contrib) line=line" contrib"
if (!has_nonfree) line=line" non-free"
print line
} else {
print
}
}' "$src" | sudo tee "$tmp" >/dev/null
sudo mv "$tmp" "$src"
run_fail "apt-get update (after enabling contrib/non-free)" sudo apt-get update
rm -rf "$TMPDIR_CREATED" || true
TMPDIR_CREATED=""
}
main() {
ensure_dir "$AUDIT_DIR"; ensure_dir "$LOG_DIR"; ensure_dir "$OUT_DIR"
: >"$LOG_FILE"
section "Start"
log "Audit directory: $AUDIT_DIR"
log "Log file : $LOG_FILE"
need_sudo
{
echo "===== ENVIRONMENT ====="
echo "Timestamp: $START_TS"
uname -a || true
echo
echo "----- /etc/os-release -----"
cat /etc/os-release 2>/dev/null || true
echo
echo "----- /proc/cmdline -----"
cat /proc/cmdline 2>/dev/null || true
echo
echo "----- CPU -----"
lscpu 2>/dev/null || true
echo
echo "----- Memory/CMA -----"
grep -E 'CmaTotal|CmaFree|MemTotal|MemFree|HugePages' /proc/meminfo 2>/dev/null || true
} >"$ENV_FILE"
run_continue "Collect dmesg" bash -lc "sudo dmesg -T > '$DMESG_FILE'"
check_platform
quick_net_check || true
section "APT update"
run_fail "apt-get update" sudo apt-get update
enable_nonfree_optional || true
section "Install baseline diagnostic tools (idempotent; skips unavailable)"
ensure_packages \
curl wget ca-certificates \
pciutils usbutils lshw hwinfo inxi \
ethtool iproute2 net-tools jq \
i2c-tools lm-sensors \
v4l-utils \
gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly \
ffmpeg alsa-utils \
vulkan-tools mesa-utils kmscube \
xrandr x11-xserver-utils autorandr \
edid-decode read-edid ddcutil \
fio hdparm nvme-cli smartmontools \
iw wireless-tools bluez can-utils
section "System overview"
run_continue "lshw (short)" sudo lshw -short
run_continue "lsblk -O" lsblk -O
run_continue "df -hT" df -hT
run_continue "lsusb -t" lsusb -t
run_continue "lspci -nnk" lspci -nnk
run_continue "Kernel warnings/errors (last 200)" bash -lc 'dmesg -T --level=err,warn | tail -n 200'
section "GPU & Display"
run_continue "GPU modules loaded" bash -lc "lsmod | egrep -i 'panthor|panfrost|mali|kbase' || true"
run_continue "GPU-related dmesg" bash -lc "dmesg -T | egrep -i 'mali|panthor|panfrost|csf|gpu' || true"
run_continue "DRM connectors (modetest -c)" modetest -c
run_continue "kmscube smoke test" bash -lc "kmscube -i 100 >/dev/null 2>&1 || true"
run_continue "Vulkan summary" vulkaninfo --summary
run_continue "xrandr --props (if X running)" bash -lc "DISPLAY=\${DISPLAY:-:0} xrandr --props 2>/dev/null || true"
section "Video / V4L2 / Codecs"
run_continue "List V4L2 devices" v4l2-ctl --list-devices
run_continue "FFmpeg hwaccels" ffmpeg -hide_banner -hwaccels
run_continue "GStreamer rockchip-ish plugins" bash -lc "gst-inspect-1.0 | egrep -i 'v4l2|rkv|hantro|rockchip' || true"
section "Audio"
run_continue "ALSA playback" aplay -l
run_continue "ALSA capture" arecord -l
section "Network"
run_continue "ip -details addr" ip -details address
run_continue "iw dev" iw dev
section "Storage"
run_continue "lsblk (model/serial)" lsblk -o NAME,SIZE,TYPE,MOUNTPOINTS,MODEL,SERIAL,TRAN
run_continue "SATA/NVMe/PCIe dmesg" bash -lc "dmesg -T | egrep -i 'sata|ahci|nvme|pcie' || true"
run_continue "nvme list (if any)" bash -lc "ls /dev/nvme*n1 >/dev/null 2>&1 && sudo nvme list || true"
section "Optional actions"
if ask_yes "Run powertop --auto-tune (changes power tunables until reboot)?"; then
run_fail "powertop --auto-tune" sudo powertop --auto-tune
fi
if ask_yes "Run sensors-detect (interactive; may load modules)?"; then
run_fail "sensors-detect" sudo sensors-detect
fi
if ask_yes "Run quick fio seq read/write in $HOME (~${FIO_SIZE_MB}MB temp file, then removed)?"; then
TMPDIR_CREATED="$(mktemp -d)"
local fiofile="$TMPDIR_CREATED/fio_test.dat"
run_fail "dd create file" dd if=/dev/zero of="$fiofile" bs=1M count="$FIO_SIZE_MB" status=none
run_fail "fio seq rw" fio --name=seqrw --filename="$fiofile" --rw=readwrite --bs="$FIO_BS" --direct=1 --numjobs=1 --iodepth="$FIO_IODEPTH" --size="${FIO_SIZE_MB}M" --group_reporting
rm -rf "$TMPDIR_CREATED" || true
TMPDIR_CREATED=""
fi
section "REPORT.md"
{
echo "# RK3588 Capability Audit — $START_TS"
echo
echo "Audit directory: $AUDIT_DIR"
echo "Log file: $LOG_FILE"
echo "Env snapshot: $ENV_FILE"
echo "dmesg: $DMESG_FILE"
} >"$REPORT_MD"
( cd "$HOME" && tar czf "${AUDIT_DIR}.tar.gz" "$(basename "$AUDIT_DIR")" )
section "Done"
log "Report : $REPORT_MD"
log "Archive: ${AUDIT_DIR}.tar.gz"
}
main "$@"
#/home/linaro/rk3588-display-toolkit/bin/rk3588_autorandr_bootstrap.sh
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# rk3588_autorandr_bootstrap.sh
#
# Fixes vs. original:
# - Works when launched as root *or* as an unprivileged user (fixes "Failed to connect to bus").
# - Protects existing preswitch hooks generated by setup scripts (Stage 1 protection).
# - Treats xrandr correctly on Debian 11 (x11-xserver-utils).
# - Performs "offline enable" for systemd units when running as root.
# - Adds a login wrapper to wait for the X11 display server.
#
# Optional inputs:
# AUTORANDR_PROFILE=<name>
# TARGET_USER=<username>
# ASSUME_YES=1
# TOOLKIT_HOME=<path>
#
# CLI flags:
# --profile <name>
# --user <username>
# --yes
AUTORANDR_PROFILE="${AUTORANDR_PROFILE:-}"
TARGET_USER="${TARGET_USER:-${SUDO_USER:-${USER:-}}}"
ASSUME_YES="${ASSUME_YES:-0}"
TOOLKIT_HOME="${TOOLKIT_HOME:-}"
log() { printf '[%(%F %T)T] %s\n' -1 "$*"; }
err() { printf '[%(%F %T)T] ERROR: %s\n' -1 "$*" >&2; }
warn() { printf '[%(%F %T)T] WARN: %s\n' -1 "$*" >&2; }
have_cmd() { command -v "$1" >/dev/null 2>&1; }
usage() {
cat <<'USAGE'
Usage: rk3588_autorandr_bootstrap.sh [--profile NAME] [--user USER] [--yes]
Environment:
AUTORANDR_PROFILE Profile to use (otherwise you will be prompted)
TARGET_USER User whose autorandr/autostart/systemd-user config will be modified
ASSUME_YES=1 Non-interactive: assume “yes” to confirmation prompts
TOOLKIT_HOME Toolkit dir (defaults to ~TARGET_USER/rk3588-display-toolkit)
USAGE
}
confirm_yes() {
local msg="$1" ans
if [[ "$ASSUME_YES" == "1" ]]; then
log "ASSUME_YES=1 -> auto-accept: $msg"
return 0
fi
echo
echo "WARNING: $msg"
read -r -p "Type 'yes' to continue, anything else to cancel: " ans
[[ "$ans" == "yes" ]] || { err "Cancelled by user."; return 1; }
}
# ----- privilege helpers -----
SUDO=""
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
have_cmd sudo || { err "sudo is required (or run as root)."; exit 1; }
sudo -v || { err "sudo authentication failed."; exit 1; }
SUDO="sudo"
fi
# ----- parse args -----
while [[ $# -gt 0 ]]; do
case "$1" in
--profile)
shift; AUTORANDR_PROFILE="${1:-}";;
--user)
shift; TARGET_USER="${1:-}";;
--yes)
ASSUME_YES=1;;
-h|--help)
usage; exit 0;;
*)
err "Unknown argument: $1"; usage; exit 2;;
esac
shift || true
done
# ----- target user/home resolution -----
TARGET_HOME=""
resolve_target_user() {
local u="$TARGET_USER"
if [[ -z "$u" ]]; then
u="$USER"
fi
# If running as root and no meaningful target is set, pick the first regular user.
if [[ "${EUID:-$(id -u)}" -eq 0 && ( -z "$u" || "$u" == "root" ) ]]; then
u="$(getent passwd | awk -F: '$3>=1000 && $3<60000 && $6 ~ /^\// {print $1; exit}')" || true
if [[ -z "$u" ]]; then
err "Running as root but no non-root TARGET_USER could be auto-detected. Set TARGET_USER=<user>."
exit 1
fi
if ! confirm_yes "You are running as root. This script will configure autorandr/autostart/user-units for user '$u'. Proceed?"; then
exit 1
fi
fi
if ! getent passwd "$u" >/dev/null; then
err "Target user does not exist: $u"
exit 1
fi
TARGET_USER="$u"
TARGET_HOME="$(getent passwd "$TARGET_USER" | awk -F: '{print $6}')"
if [[ -z "$TARGET_HOME" || ! -d "$TARGET_HOME" ]]; then
err "Could not resolve home directory for user '$TARGET_USER'"
exit 1
fi
if [[ -z "$TOOLKIT_HOME" ]]; then
TOOLKIT_HOME="$TARGET_HOME/rk3588-display-toolkit"
fi
}
run_as_target() {
if [[ "${EUID:-$(id -u)}" -eq 0 && "$TARGET_USER" != "root" ]]; then
sudo -u "$TARGET_USER" -H "$@"
else
"$@"
fi
}
chown_target() {
local path="$1"
if [[ "${EUID:-$(id -u)}" -eq 0 && "$TARGET_USER" != "root" ]]; then
chown -R "$TARGET_USER:$TARGET_USER" "$path" 2>/dev/null || true
fi
}
# ----- apt helpers -----
pkg_installed() { dpkg -s "$1" >/dev/null 2>&1; }
package_available() {
have_cmd apt-cache || return 1
apt-cache policy "$1" 2>/dev/null | awk '/Candidate:/ {print $2}' | grep -vq "(none)"
}
apt_install_if_missing() {
local pkg="$1"
if pkg_installed "$pkg"; then
log "Package already installed: $pkg"
return 0
fi
if ! package_available "$pkg"; then
log "Package not available in APT sources (skipping): $pkg"
return 0
fi
log "Installing package: $pkg"
${SUDO} DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "$pkg"
}
ensure_deps() {
log "Ensuring dependencies..."
${SUDO} apt-get update
# On Debian 11, /usr/bin/xrandr is shipped by x11-xserver-utils.
apt_install_if_missing autorandr
apt_install_if_missing x11-xserver-utils
# If the xrandr binary is still missing (rare/minimal setups), try the standalone package if the repo has it.
if ! have_cmd xrandr; then
log "xrandr binary not found after installing x11-xserver-utils; attempting standalone xrandr package if available..."
apt_install_if_missing xrandr
fi
have_cmd autorandr || { err "autorandr is still missing after install attempts."; exit 1; }
have_cmd xrandr || log "NOTE: xrandr not found. autorandr will not work in X11 sessions until xrandr is available."
}
# ----- profile selection/validation (from target user's config) -----
list_profiles() {
local dir="$TARGET_HOME/.config/autorandr"
[[ -d "$dir" ]] || return 0
find "$dir" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' 2>/dev/null | sort
}
choose_profile() {
if [[ -n "$AUTORANDR_PROFILE" ]]; then
return 0
fi
log "Available autorandr profiles for user '$TARGET_USER':"
local profiles
profiles="$(list_profiles || true)"
if [[ -z "$profiles" ]]; then
err "No autorandr profiles found in $TARGET_HOME/.config/autorandr"
err "Create one first (e.g. 'autorandr --save <name>' from within an X11 session)."
exit 1
fi
echo "$profiles" | sed 's/^/ - /'
read -r -p "Enter the profile name to use: " AUTORANDR_PROFILE
[[ -n "$AUTORANDR_PROFILE" ]] || { err "No profile provided"; exit 1; }
}
validate_profile_exists() {
local dir="$TARGET_HOME/.config/autorandr/$AUTORANDR_PROFILE"
if [[ ! -d "$dir" ]]; then
err "Profile '$AUTORANDR_PROFILE' does not exist in $TARGET_HOME/.config/autorandr"
exit 1
fi
}
# ----- installs -----
install_preswitch_hook() {
log "Installing preswitch hook template for profile: $AUTORANDR_PROFILE (user: $TARGET_USER)"
local ar_dir="$TARGET_HOME/.config/autorandr/$AUTORANDR_PROFILE"
mkdir -p "$ar_dir"
local hook="$ar_dir/preswitch"
# FIX: Don't overwrite existing hooks blindly.
# This prevents destroying the custom modeline setup generated by rk3588_dual_display_setup.sh
if [[ -s "$hook" ]]; then
warn "A preswitch hook already exists at $hook"
if ! confirm_yes "Overwrite existing preswitch hook with default template? (Select NO if you used the setup script)"; then
log "Skipping preswitch hook installation (keeping existing configuration)."
chown_target "$ar_dir"
return 0
fi
# Backup if overwriting
cp -a "$hook" "${hook}.bak.$(date +%Y%m%d_%H%M%S)" || true
fi
cat >"$hook" <<'SH'
#!/bin/sh
# autorandr preswitch hook (template)
# Runs before switching to the profile.
# Use this to (re-)add custom modelines if your hardware/firmware forgets them.
# Example:
# xrandr --newmode "1920x1080_60.00" ... || true
# xrandr --addmode HDMI-1 "1920x1080_60.00" || true
exit 0
SH
chmod +x "$hook"
chown_target "$ar_dir"
log "Hook installed: $hook"
}
install_wrapper_script() {
# A tiny helper that waits until xrandr can talk to the X server before running autorandr.
local bin_dir="$TARGET_HOME/.local/bin"
local wrapper="$bin_dir/autorandr-change"
mkdir -p "$bin_dir"
cat >"$wrapper" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
# Wait until we have a working DISPLAY and xrandr can query it.
# This makes autorandr more reliable at login across different desktop environments.
AUTORANDR_BIN="${AUTORANDR_BIN:-/usr/bin/autorandr}"
XRANDR_BIN="${XRANDR_BIN:-/usr/bin/xrandr}"
MAX_WAIT_SECONDS="${MAX_WAIT_SECONDS:-20}"
if [[ -z "${DISPLAY:-}" ]]; then
echo "autorandr-change: DISPLAY is not set; nothing to do." >&2
exit 0
fi
# Some environments don't export XAUTHORITY for systemd user units;
# if it's missing, fall back to ~/.Xauthority.
if [[ -z "${XAUTHORITY:-}" && -f "$HOME/.Xauthority" ]]; then
export XAUTHORITY="$HOME/.Xauthority"
fi
for ((i=0; i<MAX_WAIT_SECONDS; i++)); do
if "$XRANDR_BIN" --current >/dev/null 2>&1; then
exec "$AUTORANDR_BIN" --change
fi
sleep 1
done
echo "autorandr-change: timed out waiting for xrandr to become ready (DISPLAY=$DISPLAY)." >&2
exit 0
SH
chmod +x "$wrapper"
chown_target "$bin_dir"
log "Installed wrapper: $wrapper"
}
install_xdg_autostart() {
local auto_dir="$TARGET_HOME/.config/autostart"
local desktop_file="$auto_dir/autorandr-change.desktop"
mkdir -p "$auto_dir"
if [[ -f "$desktop_file" ]]; then
if ! confirm_yes "This will overwrite $desktop_file (a backup will be created). Proceed?"; then
log "Skipped XDG autostart."
return 0
fi
cp -a "$desktop_file" "${desktop_file}.bak.$(date +%Y%m%d_%H%M%S)" || true
fi
# Use wrapper to avoid race conditions at login.
cat >"$desktop_file" <<'DESKTOP'
[Desktop Entry]
Type=Application
Name=Autorandr Apply
Comment=Apply autorandr profile at login
Exec=%h/.local/bin/autorandr-change
X-GNOME-Autostart-enabled=true
DESKTOP
chown_target "$auto_dir"
log "Installed XDG autostart: $desktop_file"
}
install_systemd_user_unit() {
local unit_dir="$TARGET_HOME/.config/systemd/user"
local unit_file="$unit_dir/autorandr-change.service"
local wants_dir="$unit_dir/default.target.wants"
mkdir -p "$unit_dir" "$wants_dir"
if [[ -f "$unit_file" ]]; then
if ! confirm_yes "This will overwrite $unit_file (a backup will be created). Proceed?"; then
log "Skipped systemd user unit."
return 0
fi
cp -a "$unit_file" "${unit_file}.bak.$(date +%Y%m%d_%H%M%S)" || true
fi
cat >"$unit_file" <<'UNIT'
[Unit]
Description=Apply autorandr profile (login)
# If graphical-session.target exists in the user manager, order after it.
After=graphical-session.target
[Service]
Type=oneshot
ExecStart=%h/.local/bin/autorandr-change
[Install]
WantedBy=default.target
UNIT
# Offline enable: create the symlink systemctl --user enable would create.
ln -sf ../autorandr-change.service "$wants_dir/autorandr-change.service"
chown_target "$unit_dir"
# If we're *already* in the user's session bus, also run daemon-reload.
if run_as_target systemctl --user show-environment >/dev/null 2>&1; then
run_as_target systemctl --user daemon-reload || true
fi
log "Installed systemd user unit: $unit_file"
log "Enabled (offline) via symlink: $wants_dir/autorandr-change.service"
}
enable_system_services_if_present() {
# These units vary by distro/package; enable only if they exist.
if ${SUDO} systemctl list-unit-files 2>/dev/null | grep -q '^autorandr\.service'; then
if confirm_yes "Enable system-wide autorandr.service now?"; then
${SUDO} systemctl enable --now autorandr.service || true
fi
fi
if ${SUDO} systemctl list-unit-files 2>/dev/null | grep -q '^autorandr-resume\.service'; then
if confirm_yes "Enable system-wide autorandr-resume.service now?"; then
${SUDO} systemctl enable --now autorandr-resume.service || true
fi
fi
}
try_apply_now() {
if [[ -z "${DISPLAY:-}" ]]; then
log "DISPLAY not set; skipping immediate autorandr apply."
return 0
fi
log "DISPLAY is set; attempting autorandr apply now..."
# Prefer wrapper (waits for xrandr readiness).
local wrapper="$TARGET_HOME/.local/bin/autorandr-change"
local xauth="${XAUTHORITY:-$TARGET_HOME/.Xauthority}"
if [[ -x "$wrapper" ]]; then
if ! run_as_target env DISPLAY="$DISPLAY" XAUTHORITY="$xauth" "$wrapper"; then
err "autorandr apply failed in-session (you can run '$wrapper' manually later)."
return 0
fi
else
if ! run_as_target env DISPLAY="$DISPLAY" XAUTHORITY="$xauth" /usr/bin/autorandr --change; then
err "autorandr --change failed in-session (you can run it manually later)."
return 0
fi
fi
log "autorandr applied."
}
main() {
resolve_target_user
log "Target user: $TARGET_USER"
log "Target home: $TARGET_HOME"
ensure_deps
choose_profile
validate_profile_exists
install_preswitch_hook
install_wrapper_script
if confirm_yes "Enable system-wide autorandr services (if present)?"; then
enable_system_services_if_present
fi
if confirm_yes "Install XDG autostart entry to run autorandr at login (recommended for desktop environments)?"; then
install_xdg_autostart
fi
if confirm_yes "Install a systemd user unit to run autorandr at login (works even without DBUS during install)?"; then
install_systemd_user_unit
fi
try_apply_now
log "Done. Profile selected: $AUTORANDR_PROFILE"
log "Profile config path: $TARGET_HOME/.config/autorandr/$AUTORANDR_PROFILE"
}
main "$@"
#/home/linaro/rk3588-display-toolkit/bin/rk3588_dual_display_setup.sh
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# rk3588_dual_display_setup.sh
#
# Hard-coded dual-monitor fix (HDMI + DP) with autorandr persistence.
#
# OUT1 (default HDMI-1): 1280x720@60 primary
# OUT2 (default DP-1) : 800x600@60 right-of OUT1
# Includes SVGA (Standard VESA) fallback timing for OUT2.
#
# WARNING: This script changes display modes.
OUT1="${OUT1:-HDMI-1}"
OUT2="${OUT2:-DP-1}"
# HDMI-1: 1280x720 59.86 Hz (CVT 0.92M9)
MODE1_NAME="1280x720_60.00"
MODE1_LINE="74.50 1280 1344 1472 1664 720 723 728 748 -hsync +vsync"
# DP-1: 800x600 59.86 Hz (CVT 0.48M3) - Primary attempt
MODE2_CVT_NAME="800x600_60.00"
MODE2_CVT_LINE="38.25 800 832 912 1024 600 603 607 624 -hsync +vsync"
# DP-1: 800x600 Standard VESA - Fallback
# Note: Inferred standard VESA timings to maintain script fallback logic
MODE2_VGA_NAME="800x600_60_std"
MODE2_VGA_LINE="40.00 800 840 968 1056 600 601 605 628 +hsync +vsync"
AR_PROFILE="rk3588_dual"
log() { printf '[INFO ] %s\n' "$*"; }
err() { printf '[ERROR] %s\n' "$*" >&2; }
need_x() {
[[ -n "${DISPLAY:-}" ]] || { err "DISPLAY not set. Run from an active X session."; exit 1; }
}
need_cmd() {
command -v "$1" >/dev/null 2>&1 || { err "Missing command: $1"; exit 1; }
}
confirm_yes() {
local msg="$1" ans
echo
echo "WARNING: $msg"
read -r -p "Type 'yes' to continue, anything else to cancel: " ans
[[ "$ans" == "yes" ]] || { err "Cancelled by user."; exit 1; }
}
apply_layout() {
local right_mode="$1"
xrandr \
--output "$OUT1" --mode "$MODE1_NAME" --primary \
--output "$OUT2" --mode "$right_mode" --right-of "$OUT1"
}
main() {
need_x
need_cmd xrandr
need_cmd autorandr
confirm_yes "This will change active display modes for $OUT1 and $OUT2. Ensure you can recover via TTY."
log "Defining modelines (best-effort)..."
xrandr --newmode "$MODE1_NAME" $MODE1_LINE 2>/dev/null || true
xrandr --newmode "$MODE2_CVT_NAME" $MODE2_CVT_LINE 2>/dev/null || true
xrandr --newmode "$MODE2_VGA_NAME" $MODE2_VGA_LINE 2>/dev/null || true
xrandr --addmode "$OUT1" "$MODE1_NAME" 2>/dev/null || true
xrandr --addmode "$OUT2" "$MODE2_CVT_NAME" 2>/dev/null || true
xrandr --addmode "$OUT2" "$MODE2_VGA_NAME" 2>/dev/null || true
log "Applying layout (try CVT timing first)..."
if ! apply_layout "$MODE2_CVT_NAME"; then
log "CVT timing failed; trying VESA/SVGA fallback..."
apply_layout "$MODE2_VGA_NAME"
fi
log "Creating autorandr preswitch hook for persistence..."
local ar_dir="$HOME/.config/autorandr/$AR_PROFILE"
mkdir -p "$ar_dir"
cat >"$ar_dir/preswitch" <<SH
#!/bin/sh
xrandr --newmode "$MODE1_NAME" $MODE1_LINE 2>/dev/null || true
xrandr --newmode "$MODE2_CVT_NAME" $MODE2_CVT_LINE 2>/dev/null || true
xrandr --newmode "$MODE2_VGA_NAME" $MODE2_VGA_LINE 2>/dev/null || true
xrandr --addmode "$OUT1" "$MODE1_NAME" 2>/dev/null || true
xrandr --addmode "$OUT2" "$MODE2_CVT_NAME" 2>/dev/null || true
xrandr --addmode "$OUT2" "$MODE2_VGA_NAME" 2>/dev/null || true
SH
chmod +x "$ar_dir/preswitch"
log "Saving autorandr profile '$AR_PROFILE' (forced)..."
autorandr --save "$AR_PROFILE" --force
log "Done. Profile saved: $AR_PROFILE"
}
main "$@"
#/home/linaro/my_custom_display_config.sh
#!/bin/bash
# Xrandr dual-monitor setup (expanded, refactored)
# HDMI-1 → 1280x720@60 (primary)
# DP-1 → 800x600@60 (right of HDMI-1)
# Modelines and outputs are fixed based on provided logs. No placeholders.
set -Eeuo pipefail
# --------------------------
# Utilities / Logging
# --------------------------
log() { printf '[INFO ] %s\n' "$*"; }
warn() { printf '[WARN ] %s\n' "$*" >&2; }
err() { printf '[ERROR] %s\n' "$*" >&2; }
# If DRY_RUN=1 is set in the environment, print commands instead of executing them.
: "${DRY_RUN:=0}"
run() {
if [[ "$DRY_RUN" == "1" ]]; then
printf '[DRYRUN] %s\n' "$*"
return 0
fi
eval "$@"
}
# --------------------------
# Pre-flight checks
# --------------------------
if ! command -v xrandr >/dev/null 2>&1; then
err 'xrandr not found'
exit 127
fi
# --------------------------
# Fixed configuration from logs
# --------------------------
# Data derived from: Modeline "1280x720_60.00" 74.50 1280... -hsync +vsync
OUTPUT_1_NAME="HDMI-1"
MODE_1_NAME="1280x720_60.00"
# Params: Clock HDisp HSyncStart HSyncEnd HTotal VDisp VSyncStart VSyncEnd VTotal Flags
MODELINE_1_PARAMS="74.50 1280 1344 1472 1664 720 723 728 748 -hsync +vsync"
# Data derived from: Modeline "800x600_60.00" 38.25 800... -hsync +vsync
OUTPUT_2_NAME="DP-1"
MODE_2_NAME="800x600_60.00"
# Params: Clock HDisp HSyncStart HSyncEnd HTotal VDisp VSyncStart VSyncEnd VTotal Flags
MODELINE_2_PARAMS="38.25 800 832 912 1024 600 603 607 624 -hsync +vsync"
# --------------------------
# Helpers for xrandr state
# --------------------------
connected() {
xrandr | grep -q "^$1\s*connected"
}
mode_defined() {
local name="$1"
# A mode is "defined" if it appears anywhere in xrandr --query output
xrandr --query | awk '{print $1}' | grep -Fxq "$name"
}
output_has_mode() {
local out="$1" name="$2"
# Extract the block for this output and list the mode names, then search for ours.
xrandr --query | awk -v o="$out" '
$1==o {on=1; next}
on && NF==0 {on=0}
on && $1!~/^\t/ && $2=="connected" {on=0}
on {print $1}
' | sed 's/^[[:space:]]*//' | grep -Fxq "$name" 2>/dev/null
}
active_geom() {
# Prints WxH for given output if connected; empty otherwise
local out="$1"
xrandr --query | awk -v o="$out" '$1==o && $2=="connected" {
match($0, /[0-9]+x[0-9]+\+[0-9]+\+[0-9]+/);
if (RSTART) { s=substr($0,RSTART,RLENGTH); sub(/\+.*/, "", s); print s; }
}'
}
# --------------------------
# Capture pre-state for reversible changes
# --------------------------
PRESTATE_CMD=()
for out in "$OUTPUT_1_NAME" "$OUTPUT_2_NAME"; do
if connected "$out"; then
# Best-effort revert uses preferred mode and auto placement for each output.
PRESTATE_CMD+=(--output "$out" --auto)
fi
done
revert() {
if ((${#PRESTATE_CMD[@]})); then
warn "Reverting to auto configuration"
run xrandr "${PRESTATE_CMD[@]}" || warn "Revert failed"
fi
}
trap 'err "Failure occurred"; revert' ERR
# --------------------------
# Ensure modes exist and are assigned to outputs
# --------------------------
ensure_mode_defined() {
local name="$1"; shift
if mode_defined "$name"; then
# NOTE: This check only verifies the NAME exists. It does NOT verify if the
# parameters match. If you changed modeline params but kept the name,
# you must delete the mode manually or rename it.
log "Mode present: $name (skipping definition)"
else
log "Defining mode: $name"
run xrandr --newmode "$name" "$@"
fi
}
ensure_output_has_mode() {
local out="$1" name="$2"
if output_has_mode "$out" "$name"; then
log "Output $out already has mode $name"
else
log "Adding mode $name to $out"
run xrandr --addmode "$out" "$name"
fi
}
# Validate outputs are connected before modifying
for out in "$OUTPUT_1_NAME" "$OUTPUT_2_NAME"; do
if ! connected "$out"; then
err "Output not connected: $out"
exit 2
fi
done
# Define modes idempotently
ensure_mode_defined "$MODE_1_NAME" $MODELINE_1_PARAMS
ensure_mode_defined "$MODE_2_NAME" $MODELINE_2_PARAMS
# Assign modes to each output
ensure_output_has_mode "$OUTPUT_1_NAME" "$MODE_1_NAME"
ensure_output_has_mode "$OUTPUT_2_NAME" "$MODE_2_NAME"
# --------------------------
# Apply requested layout
# --------------------------
LAYOUT_CMD=(
--output "$OUTPUT_1_NAME" --mode "$MODE_1_NAME" --primary
--output "$OUTPUT_2_NAME" --mode "$MODE_2_NAME" --right-of "$OUTPUT_1_NAME"
)
log "Applying layout"
run xrandr "${LAYOUT_CMD[@]}"
# --------------------------
# Post-apply verification
# --------------------------
geom1=$(active_geom "$OUTPUT_1_NAME")
geom2=$(active_geom "$OUTPUT_2_NAME")
if [[ "$geom1" != "1280x720" ]]; then
err "Post-check failed: $OUTPUT_1_NAME geometry is '$geom1' (expected 1280x720)"
revert; exit 3
fi
if [[ "$geom2" != "800x600" ]]; then
err "Post-check failed: $OUTPUT_2_NAME geometry is '$geom2' (expected 800x600)"
revert; exit 4
fi
log "Configuration successful: $OUTPUT_1_NAME=$geom1 (primary), $OUTPUT_2_NAME=$geom2 (right)"
exit 0