赞美 grobi:自动配置 X11 显示器
赞美 grobi:自动配置 X11 显示器
目录:
最近我又开始使用 Alexander Neumann 编写的 grobi
程序了,并且很高兴地发现它能更方便地使用我那不太好伺候(但很棒)的 Dell 32-inch 8K monitor (UP3218K) 显示器了。与我之前基于休眠的方法相比,我能更快地获得信号。
以前,当我的 PC 从 suspend-to-RAM 唤醒时,会有两种情况:
- 显示器已连接。我的 sleep program 会打开显示器电源(如果需要),短暂休眠,然后运行
xrandr(1)
来(希望)正确配置显示器。 - 显示器未连接,例如因为它仍然连接到我的工作 PC。
在情况 ② 中,或者如果情况 ① 中的单次配置尝试失败,我需要从另一台计算机 SSH 登录并手动运行 xrandr
,以便显示器显示信号:
% DISPLAY=:0 xrandr \
--output DP-4 --mode 3840x4320 --panning 0x0+0+0 \
--output DP-2 --right-of DP-4 --mode 3840x4320 --panning 0x0+3840+0
使用 grobi 自动配置显示器
现在,我通过创建以下 ~/.config/grobi.conf
文件完全解决了这个问题:
rules:- name:UP3218Koutputs_connected:[DP-2, DP-4]# DP-4 is left, DP-2 is rightconfigure_row:- DP-4@3840x4320- DP-2@3840x4320# atomic instructs grobi to only call xrandr once and configure all the# outputs. This does not always work with all graphic cards, but is# needed to successfully configure the UP3218K monitor.atomic:true
…并使用以下命令安装/启用 grobi
(在 Arch Linux 上):
% sudo pacman -S grobi
% systemctl --user enable --now grobi
每当 grobi
检测到我的显示器已连接(它监听 X11 RandR 输出更改事件)时,它将运行 xrandr(1)
来配置显示器的分辨率和位置。
要检查 grobi
正在看到/做的事情,你可以使用:
% systemctl --user status grobi
% journalctl --user -u grobi
例如,在我的系统上,我看到:
grobi: 18:31:48.823765 outputs: [HDMI-0 (primary) DP-0 DP-1 DP-2 (connected) 3840x2160+ [DEL-16711-808727372-DELL UP3218K-D2HP805I043L] DP-3 DP-4 (connected) 3840x21>
grobi: 18:31:48.823783 new rule found: UP3218K
grobi: 18:31:48.823785 enable outputs: [DP-4@3840x4320 DP-2@3840x4320]
grobi: 18:31:48.823789 using one atomic call to xrandr
grobi: 18:31:48.823806 running command /usr/bin/xrandr xrandr --output DP-4 --mode 3840x4320 --output DP-2 --mode 3840x4320 --right-of DP-4
grobi: 18:31:49.285944 new RANDR change event received
值得注意的是,现在摆脱糟糕状态(无信号)的说明是关闭显示器电源,然后重新打开。这将导致 RandR 输出更改事件,从而触发 grobi
,进而运行 xrandr
,然后配置显示器。 妙!
为什么不用 autorandr?
没有特别的原因。我只是知道 grobi
。
此外,grobi
是用 Go 编写的,因此它可能会在未来几年内保持平稳运行。
grobi 是否支持 Wayland?
可能不支持。 在 grobi repository 上没有提到 Wayland。
额外内容:我的 Suspend-to-RAM 设置
作为奖励,本节介绍了我的显示器相关自动化的另一半。
当我将 PC 挂起到 RAM 时,我希望稍后手动唤醒它,例如通过按键盘上的键或发送 Wake-on-LAN 数据包,或者我希望它每天早上 6:50 自动唤醒——这样,每日 cron 作业可以在我开始使用计算机之前运行一段时间。
为了实现这一点,我使用 zleep
,它是 rtcwake(8)
和 systemctl suspend
的包装程序,它与 myStrom switch 智能插头集成,以完全关闭显示器的电源。 这是值得的,因为即使在待机状态下,显示器也会消耗 30W 的功率!
package main
import (
"context"
"flag"
"fmt"
"log"
"net/http"
"net/url"
"os"
"os/exec"
"time"
)
var (
resume = flag.Bool("resume",
false,
"run resume behavior only (turn on monitor via smart plug)")
noMonitor = flag.Bool("no_monitor",
false,
"disable turning off/on monitor")
)
func monitorPower(ctx context.Context, method, cmnd string) error {
if *noMonitor {
log.Printf("[monitor power] skipping because -no_monitor flag is set")
return nil
}
log.Printf("[monitor power] command: %v", cmnd)
u, err := url.Parse("http://myStrom-Switch-A46FD0/" + cmnd)
if err != nil {
return err
}
for {
if err := ctx.Err(); err != nil {
return err
}
req, err := http.NewRequest(method, u.String(), nil)
if err != nil {
return err
}
ctx, canc := context.WithTimeout(ctx, 5*time.Second)
defer canc()
req = req.WithContext(ctx)
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Print(err)
time.Sleep(1 * time.Second)
continue
}
if resp.StatusCode != http.StatusOK {
log.Printf("unexpected HTTP status code: got %v, want %v", resp.Status, http.StatusOK)
time.Sleep(1 * time.Second)
continue
}
log.Printf("[monitor power] request succeeded")
return nil
}
}
func nextWakeup(now time.Time) time.Time {
midnight := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
if now.Hour() < 6 {
// wake up today
return midnight.Add(6*time.Hour + 50*time.Minute)
}
// wake up tomorrow
return midnight.Add(24 * time.Hour).Add(6*time.Hour + 50*time.Minute)
}
func runResume() error {
// Retry for up to one minute to give the network some time to come up
ctx, canc := context.WithTimeout(context.Background(), 1*time.Minute)
defer canc()
if err := monitorPower(ctx, "GET", "relay?state=1"); err != nil {
log.Print(err)
}
return nil
}
func zleep() error {
ctx := context.Background()
now := time.Now().Truncate(1 * time.Second)
wakeup := nextWakeup(now)
log.Printf("now : %v", now)
log.Printf("wakeup: %v", wakeup)
log.Printf("wakeup: %v (timestamp)", wakeup.Unix())
// assumes hwclock is running in UTC (see timedatectl | grep local)
// Power the monitor off in 15 seconds.
// mode=on is intentional: https://api.mystrom.ch/#e532f952-36ea-40fb-a180-a57b835f550e
// - the switch will be turned on (already on, so this is a no-op)
// - the switch will wait for 15 seconds
// - the switch will be turned off
if err := monitorPower(ctx, "POST", "timer?mode=on&time=15"); err != nil {
log.Print(err)
}
sleep := exec.Command("sh", "-c", fmt.Sprintf("sudo rtcwake -m no --verbose --utc -t %v && sudo systemctl suspend", wakeup.Unix()))
sleep.Stdout = os.Stdout
sleep.Stderr = os.Stderr
fmt.Printf("running %v\n", sleep.Args)
if err := sleep.Run(); err != nil {
return fmt.Errorf("%v: %v", sleep.Args, err)
}
return nil
}
func main() {
flag.Parse()
if *resume {
if err := runResume(); err != nil {
log.Fatal(err)
}
} else {
if err := zsleep(); err != nil {
log.Fatal(err)
}
}
}
为了在恢复后打开显示器的电源,我将以下 shell 脚本放在 /lib/systemd/system-sleep/zleep.sh
中:
#!/bin/sh
case "$1" in
pre) exit 0
;;
post) /usr/local/bin/zleep -resume
exit 0
;;
*) exit 1
;;
esac
一旦电源打开,grobi 将检测并配置显示器。
这是该程序在运行中的示例:
2025/05/06 21:58:32 now : 2025-05-06 21:58:32 +0200 CEST
2025/05/06 21:58:32 wakeup: 2025-05-07 06:50:00 +0200 CEST
2025/05/06 21:58:32 wakeup: 1746593400 (timestamp)
2025/05/06 21:58:32 [monitor power] command: timer?mode=on&time=15
2025/05/06 21:58:32 [monitor power] request succeeded
running [sh -c sudo rtcwake -m no --verbose --utc -t 1746593400 && sudo systemctl suspend]
Using UTC time.
delta = 0
tzone = 0
tzname = UTC
systime = 1746561512, (UTC) Tue May 6 19:58:32 2025
rtctime = 1746561512, (UTC) Tue May 6 19:58:32 2025
alarm 1746593400, sys_time 1746561512, rtc_time 1746561512, seconds 0
rtcwake: wakeup using /dev/rtc0 at Wed May 7 04:50:00 2025
suspend mode: no; leaving