From dc932b8f645af3c3957abc68a7baa516ebe0c0f0 Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 25 Apr 2026 13:20:58 +0200 Subject: [PATCH] Initial Fedora brightness automation setup --- .gitignore | 3 + LICENSE | 21 + README.md | 152 +++++ bin/brightness-down-all | 14 + bin/brightness-osd | 204 +++++++ bin/brightness-sync-hg342pcb | 65 +++ bin/brightness-up-all | 14 + docs/hp-firefly-fedora-brightness-notes.md | 522 ++++++++++++++++++ install.sh | 59 ++ systemd/user/external-brightness-sync.service | 13 + uninstall.sh | 17 + wluma/config.toml | 8 + 12 files changed, 1092 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100755 bin/brightness-down-all create mode 100755 bin/brightness-osd create mode 100755 bin/brightness-sync-hg342pcb create mode 100755 bin/brightness-up-all create mode 100644 docs/hp-firefly-fedora-brightness-notes.md create mode 100755 install.sh create mode 100644 systemd/user/external-brightness-sync.service create mode 100755 uninstall.sh create mode 100644 wluma/config.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a5bb25 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.py[cod] +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..005892d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Jan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7e0dc6a --- /dev/null +++ b/README.md @@ -0,0 +1,152 @@ +# Fedora Brightness Automation Buttons OSD + +Small Fedora/GNOME brightness setup for laptops with an internal Linux backlight and an external DDC/CI monitor. + +The internal display is the master. Brightness keys change the internal panel, show a compact OSD, and a user service mirrors the same percentage to the external monitor with `ddcutil`. + +This avoids drift from letting two outputs auto-adjust independently. + +## Features + +- Internal display controlled with `brightnessctl` +- External monitor mirrored with `ddcutil` VCP code `10` +- Compact GTK4/libadwaita OSD for custom brightness shortcuts +- Optional Wayland top-edge positioning with `gtk4-layer-shell` +- systemd user service for continuous external sync +- Configurable backlight device, DDC bus, monitor model, step size, and sync interval +- Optional wluma config where only the internal display is managed + +## Tested Setup + +- Fedora 43 +- GNOME Shell 49 +- Internal panel: `intel_backlight` +- External monitor: DDC/CI via `ddcutil` + +Other Fedora/GNOME setups should work if `brightnessctl` and `ddcutil` can control the displays. + +## Dependencies + +```bash +sudo dnf install -y brightnessctl ddcutil python3-gobject gtk4 libadwaita gtk4-layer-shell +``` + +`gtk4-layer-shell` is recommended on GNOME Wayland. Without it, GNOME may place the OSD like a normal GTK window instead of pinning it at the top edge. + +If your `ddcutil` command needs sudo, configure passwordless sudo for the specific command you use. Example: + +```text +jan ALL=(root) NOPASSWD: /usr/bin/ddcutil +``` + +Use a stricter sudoers rule if you prefer limiting arguments. + +## Quick Install + +```bash +git clone https://gitea.diehanis.de/Jan/Fedora_Brightness_Automation_Buttons_OSD.git +cd Fedora_Brightness_Automation_Buttons_OSD +./install.sh +``` + +The installer writes: + +```text +~/.local/bin/brightness-osd +~/.local/bin/brightness-up-all +~/.local/bin/brightness-down-all +~/.local/bin/brightness-sync-hg342pcb +~/.config/systemd/user/external-brightness-sync.service +~/.config/brightness-automation/env +``` + +Then set your GNOME custom keyboard shortcuts to: + +```text +/home/YOUR_USER/.local/bin/brightness-down-all +/home/YOUR_USER/.local/bin/brightness-up-all +``` + +Use the full path in GNOME Settings. + +## Configuration + +Edit: + +```text +~/.config/brightness-automation/env +``` + +Default config: + +```bash +BRIGHTNESS_BACKLIGHT_DEVICE="intel_backlight" +BRIGHTNESS_SYNC_BACKLIGHT="/sys/class/backlight/intel_backlight" +BRIGHTNESS_STEP="10" +BRIGHTNESS_DDCUTIL_MODEL="HG342PCB" +BRIGHTNESS_DDCUTIL_DISPLAY="1" +BRIGHTNESS_DDCUTIL_BUS="16" +BRIGHTNESS_SYNC_INTERVAL="1" +BRIGHTNESS_SYNC_MIN_PERCENT="1" +BRIGHTNESS_OSD_VISIBLE_MS="1400" +``` + +After changing DDC or sync values: + +```bash +systemctl --user restart external-brightness-sync.service +``` + +## Finding Your Values + +Backlight device: + +```bash +brightnessctl -l +ls /sys/class/backlight +``` + +External monitor: + +```bash +sudo ddcutil detect +sudo ddcutil getvcp 10 +``` + +If `ddcutil detect` reports a bus like `/dev/i2c-16`, set: + +```bash +BRIGHTNESS_DDCUTIL_BUS="16" +``` + +## wluma + +If you use `wluma`, let it manage only the internal display. The external monitor should follow through the sync service. + +Optional install of the example wluma config: + +```bash +INSTALL_WLUMA_CONFIG=1 ./install.sh +systemctl --user restart wluma.service +``` + +Example config is in: + +```text +wluma/config.toml +``` + +## Status + +```bash +systemctl --user status external-brightness-sync.service +journalctl --user -u external-brightness-sync.service -n 50 --no-pager +``` + +## Uninstall + +```bash +./uninstall.sh +``` + +The uninstall script keeps `~/.config/brightness-automation/env` so local settings are not destroyed. diff --git a/bin/brightness-down-all b/bin/brightness-down-all new file mode 100755 index 0000000..e7b1a69 --- /dev/null +++ b/bin/brightness-down-all @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +CONFIG="${BRIGHTNESS_AUTOMATION_CONFIG:-$HOME/.config/brightness-automation/env}" +if [[ -f "$CONFIG" ]]; then + # shellcheck disable=SC1090 + source "$CONFIG" +fi + +BACKLIGHT_DEVICE="${BRIGHTNESS_BACKLIGHT_DEVICE:-intel_backlight}" +STEP="${BRIGHTNESS_STEP:-10}" + +brightnessctl -q -d "$BACKLIGHT_DEVICE" set "${STEP}%-" +"$HOME/.local/bin/brightness-osd" || true diff --git a/bin/brightness-osd b/bin/brightness-osd new file mode 100755 index 0000000..3d48cbb --- /dev/null +++ b/bin/brightness-osd @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +import os +import signal +import sys +import time + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +from gi.repository import Adw, Gdk, GLib, Gtk + +try: + gi.require_version("Gtk4LayerShell", "1.0") + from gi.repository import Gtk4LayerShell +except (ImportError, ValueError): + Gtk4LayerShell = None + + +BACKLIGHT_PATH = os.environ.get( + "BRIGHTNESS_OSD_BACKLIGHT", "/sys/class/backlight/intel_backlight" +) +PID_FILE = os.environ.get("BRIGHTNESS_OSD_PID", "/tmp/brightness-osd.pid") +VALUE_FILE = os.environ.get("BRIGHTNESS_OSD_VALUE", "/tmp/brightness-osd.value") +VISIBLE_MS = int(os.environ.get("BRIGHTNESS_OSD_VISIBLE_MS", "1400")) + + +def read_percent(): + if len(sys.argv) > 1: + return max(0, min(100, int(round(float(sys.argv[1]))))) + + with open(os.path.join(BACKLIGHT_PATH, "brightness"), encoding="utf-8") as handle: + current = int(handle.read().strip()) + + with open(os.path.join(BACKLIGHT_PATH, "max_brightness"), encoding="utf-8") as handle: + maximum = int(handle.read().strip()) + + return max(0, min(100, round(current * 100 / maximum))) + + +def read_existing_pid(): + try: + with open(PID_FILE, encoding="utf-8") as handle: + return int(handle.read().strip()) + except (FileNotFoundError, ValueError): + return None + + +def process_exists(pid): + try: + os.kill(pid, 0) + return True + except ProcessLookupError: + return False + except PermissionError: + return True + + +def signal_existing(percent): + pid = None + for _attempt in range(5): + pid = read_existing_pid() + if pid and pid != os.getpid() and process_exists(pid): + break + time.sleep(0.03) + else: + return False + + with open(VALUE_FILE, "w", encoding="utf-8") as handle: + handle.write(str(percent)) + + try: + os.kill(pid, signal.SIGUSR1) + return True + except ProcessLookupError: + return False + + +class BrightnessOsd(Adw.Application): + def __init__(self, percent): + super().__init__(application_id="local.brightness.osd") + self.percent = percent + self.label = None + self.bar = None + self.timeout_id = None + + def update_percent(self, percent): + self.percent = max(0, min(100, percent)) + + if self.label: + self.label.set_label(f"{self.percent}%") + if self.bar: + self.bar.set_value(self.percent) + + if self.timeout_id: + GLib.source_remove(self.timeout_id) + self.timeout_id = GLib.timeout_add(VISIBLE_MS, self.quit) + + def handle_refresh(self, *_args): + try: + with open(VALUE_FILE, encoding="utf-8") as handle: + percent = int(handle.read().strip()) + except (FileNotFoundError, ValueError): + percent = read_percent() + + GLib.idle_add(self.update_percent, percent) + + def do_activate(self): + with open(PID_FILE, "w", encoding="utf-8") as handle: + handle.write(str(os.getpid())) + + window = Gtk.ApplicationWindow(application=self) + window.set_title("Brightness") + window.set_decorated(False) + window.set_resizable(False) + window.set_default_size(190, 58) + window.set_opacity(0.92) + window.set_hide_on_close(True) + + if Gtk4LayerShell is not None: + Gtk4LayerShell.init_for_window(window) + Gtk4LayerShell.set_layer(window, Gtk4LayerShell.Layer.OVERLAY) + Gtk4LayerShell.set_anchor(window, Gtk4LayerShell.Edge.TOP, True) + Gtk4LayerShell.set_margin(window, Gtk4LayerShell.Edge.TOP, 28) + Gtk4LayerShell.set_keyboard_mode(window, Gtk4LayerShell.KeyboardMode.NONE) + + css = Gtk.CssProvider() + css.load_from_data( + b""" + window { + background: transparent; + } + .osd { + background: alpha(@window_fg_color, 0.88); + color: @window_bg_color; + border-radius: 15px; + padding: 10px 14px; + box-shadow: 0 10px 34px alpha(black, 0.34); + } + .osd image { + color: @window_bg_color; + } + .percent { + font-size: 15px; + font-weight: 700; + } + levelbar trough { + min-height: 6px; + border-radius: 999px; + background: alpha(@window_bg_color, 0.24); + } + levelbar block.filled { + min-height: 6px; + border-radius: 999px; + background: @window_bg_color; + } + """ + ) + Gtk.StyleContext.add_provider_for_display( + Gdk.Display.get_default(), css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ) + + box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) + box.add_css_class("osd") + box.set_halign(Gtk.Align.CENTER) + box.set_valign(Gtk.Align.START) + box.set_margin_top(28) + + icon = Gtk.Image.new_from_icon_name("display-brightness-symbolic") + icon.set_pixel_size(22) + + self.label = Gtk.Label(label=f"{self.percent}%") + self.label.add_css_class("percent") + self.label.set_width_chars(4) + + self.bar = Gtk.LevelBar.new_for_interval(0, 100) + self.bar.set_value(self.percent) + self.bar.set_size_request(96, -1) + self.bar.set_valign(Gtk.Align.CENTER) + + box.append(icon) + box.append(self.bar) + box.append(self.label) + window.set_child(box) + + signal.signal(signal.SIGUSR1, self.handle_refresh) + window.present() + self.timeout_id = GLib.timeout_add(VISIBLE_MS, self.quit) + + +def main(): + percent = read_percent() + if signal_existing(percent): + return 0 + + with open(PID_FILE, "w", encoding="utf-8") as handle: + handle.write(str(os.getpid())) + + app = BrightnessOsd(percent) + return app.run([]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/bin/brightness-sync-hg342pcb b/bin/brightness-sync-hg342pcb new file mode 100755 index 0000000..efcf8ed --- /dev/null +++ b/bin/brightness-sync-hg342pcb @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail + +CONFIG="${BRIGHTNESS_AUTOMATION_CONFIG:-$HOME/.config/brightness-automation/env}" +if [[ -f "$CONFIG" ]]; then + # shellcheck disable=SC1090 + source "$CONFIG" +fi + +MODEL="${BRIGHTNESS_DDCUTIL_MODEL:-HG342PCB}" +DISPLAY_NUM="${BRIGHTNESS_DDCUTIL_DISPLAY:-1}" +BUS_NUM="${BRIGHTNESS_DDCUTIL_BUS:-16}" +BACKLIGHT_PATH="${BRIGHTNESS_SYNC_BACKLIGHT:-/sys/class/backlight/intel_backlight}" +INTERVAL="${BRIGHTNESS_SYNC_INTERVAL:-1}" +MIN_PERCENT="${BRIGHTNESS_SYNC_MIN_PERCENT:-1}" + +brightness_percent() { + local current max percent + + current="$(<"${BACKLIGHT_PATH}/brightness")" + max="$(<"${BACKLIGHT_PATH}/max_brightness")" + percent="$(( (current * 100 + max / 2) / max ))" + + if (( percent < MIN_PERCENT )); then + percent="$MIN_PERCENT" + elif (( percent > 100 )); then + percent=100 + fi + + printf '%s\n' "$percent" +} + +run_ddcutil() { + if sudo -n ddcutil --bus "$BUS_NUM" getvcp 10 >/dev/null 2>&1; then + sudo -n ddcutil --bus "$BUS_NUM" "$@" + return + fi + + if sudo -n ddcutil --display "$DISPLAY_NUM" getvcp 10 >/dev/null 2>&1; then + sudo -n ddcutil --display "$DISPLAY_NUM" "$@" + return + fi + + if sudo -n ddcutil --model "$MODEL" getvcp 10 >/dev/null 2>&1; then + sudo -n ddcutil --model "$MODEL" "$@" + return + fi + + echo "External ${MODEL} was not reachable via sudo -n ddcutil." >&2 + return 1 +} + +last_percent="" + +while true; do + percent="$(brightness_percent)" + + if [[ "$percent" != "$last_percent" ]]; then + if run_ddcutil setvcp 10 "$percent" >/dev/null; then + last_percent="$percent" + fi + fi + + sleep "$INTERVAL" +done diff --git a/bin/brightness-up-all b/bin/brightness-up-all new file mode 100755 index 0000000..27a9daa --- /dev/null +++ b/bin/brightness-up-all @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +CONFIG="${BRIGHTNESS_AUTOMATION_CONFIG:-$HOME/.config/brightness-automation/env}" +if [[ -f "$CONFIG" ]]; then + # shellcheck disable=SC1090 + source "$CONFIG" +fi + +BACKLIGHT_DEVICE="${BRIGHTNESS_BACKLIGHT_DEVICE:-intel_backlight}" +STEP="${BRIGHTNESS_STEP:-10}" + +brightnessctl -q -d "$BACKLIGHT_DEVICE" set "+${STEP}%" +"$HOME/.local/bin/brightness-osd" || true diff --git a/docs/hp-firefly-fedora-brightness-notes.md b/docs/hp-firefly-fedora-brightness-notes.md new file mode 100644 index 0000000..2a00845 --- /dev/null +++ b/docs/hp-firefly-fedora-brightness-notes.md @@ -0,0 +1,522 @@ +# HP Firefly G11 unter Fedora – finaler sauberer Stand + +## Ziel +Saubere gemeinsame Helligkeitssteuerung für: + +- internes Display über `intel_backlight` +- externen Monitor über `ddcutil` + +sowie automatische Helligkeitsregelung über den vorhandenen Ambient-Light-Sensor mit `wluma`. + +Finale Logik: +- **internes Display ist Master** +- `wluma` regelt nur noch `intel_backlight` +- der externe Monitor wird automatisch auf denselben Prozentwert nachgezogen +- die manuellen Helligkeitstasten zeigen ein GNOME/Adwaita-passendes OSD +- dadurch können intern und extern nicht mehr durch zwei unabhängige `wluma`-Regelungen auseinanderlaufen + +--- + +## Finaler Ist-Zustand + +### Was aktuell funktioniert +- Das interne Display hängt als normales Backlight unter: + - `intel_backlight` +- Der externe Monitor `HG342PCB` am Thunderbolt-Dock ist per `ddcutil` erreichbar. +- Die gemeinsamen Button-Skripte regeln jetzt: + - intern per `brightnessctl` + - zeigen ein lokales GTK/libadwaita-OSD + - extern folgt automatisch über `external-brightness-sync.service` +- `wluma.service` läuft als User-Service nach dem Login. +- `wluma` nutzt nur noch das interne `intel_backlight`. +- `external-brightness-sync.service` spiegelt den internen Prozentwert automatisch auf den externen Monitor per `ddcutil`. + +### Aktueller Nachweis +```bash +systemctl --user status wluma.service +systemctl --user status external-brightness-sync.service +brightnessctl -l +sudo ddcutil detect +sudo ddcutil getvcp 10 +``` + +Erwarteter sauberer Zustand: +in `brightnessctl -l`: +- `intel_backlight` + +und in `sudo ddcutil detect`: +- `Display 1` +- `I2C bus: /dev/i2c-16` +- `Model: HG342PCB` + +--- + +## Warum dieser Stand der richtige ist + +### 1. Internes und externes Display laufen jetzt über zwei stabile Wege +Das interne Panel läuft weiter über `intel_backlight`. +Der externe Monitor wird über das Thunderbolt-Dock direkt per `ddcutil` angesprochen. + +Damit ist die gemeinsame Steuerung wieder stabil, auch wenn intern und extern technisch über unterschiedliche Wege laufen. + +Wichtig: +Es gibt nur noch eine automatische Regelinstanz. `wluma` regelt das interne Panel, der Sync-Dienst übernimmt die Spiegelung nach extern. + +### 2. Das Thunderbolt-Dock reicht DDC/CI sauber durch, `ddcci_backlight` war hier aber nicht der richtige Weg +Mit dem neuen Dock ist der Monitor nachweislich über `ddcutil` auf `card1-DP-5` / `/dev/i2c-16` erreichbar. +Die frühere `ddcci3`-Lösung über `/sys/class/backlight` war für dieses Setup dagegen nicht stabil nutzbar. + +Der saubere Endstand ist deshalb: + +- **intern: `brightnessctl`** +- **extern: `ddcutil`** +- **kein aggressives Hotplug-Refresh über `ddcci`** + +### 3. GNOME Auto Brightness war nicht funktionsfähig +Der Ambient-Light-Sensor funktioniert eindeutig, aber GNOME hat die Helligkeit sichtbar nicht geregelt. +Deshalb ist `wluma` jetzt die saubere automatische Lösung auf Basis des Sensors. + +--- + +## Aktive Komponenten + +### Als User aktiv + +#### wluma-Service +```text +~/.config/systemd/user/wluma.service +``` + +#### wluma-Konfiguration +```text +~/.config/wluma/config.toml +``` + +#### Gemeinsame Helligkeitsskripte +```text +~/.local/bin/brightness-osd +~/.local/bin/brightness-ddcutil-hg342pcb +~/.local/bin/brightness-down-all +~/.local/bin/brightness-up-all +``` + +#### Externer Sync-Dienst +```text +~/.local/bin/brightness-sync-hg342pcb +~/.config/systemd/user/external-brightness-sync.service +``` + +--- + +## Finaler Inhalt der wichtigen Dateien + +### 1. `~/.local/bin/brightness-ddcutil-hg342pcb` +```bash +#!/usr/bin/env bash +set -euo pipefail + +MODEL="${BRIGHTNESS_DDCUTIL_MODEL:-HG342PCB}" +STEP="${BRIGHTNESS_DDCUTIL_STEP:-10}" +DISPLAY_NUM="${BRIGHTNESS_DDCUTIL_DISPLAY:-1}" +BUS_NUM="${BRIGHTNESS_DDCUTIL_BUS:-16}" + +if [ "$#" -ne 1 ]; then + echo "Usage: $(basename "$0") up|down" >&2 + exit 2 +fi + +case "$1" in + up) delta="+" ;; + down) delta="-" ;; + *) echo "Usage: $(basename "$0") up|down" >&2; exit 2 ;; +esac + +run_ddcutil() { + if sudo -n ddcutil --bus "$BUS_NUM" getvcp 10 >/dev/null 2>&1; then + sudo -n ddcutil --bus "$BUS_NUM" "$@" + return + fi + + if sudo -n ddcutil --display "$DISPLAY_NUM" getvcp 10 >/dev/null 2>&1; then + sudo -n ddcutil --display "$DISPLAY_NUM" "$@" + return + fi + + if sudo -n ddcutil --model "$MODEL" getvcp 10 >/dev/null 2>&1; then + sudo -n ddcutil --model "$MODEL" "$@" + return + fi + + if ddcutil --bus "$BUS_NUM" getvcp 10 >/dev/null 2>&1; then + ddcutil --bus "$BUS_NUM" "$@" + return + fi + + if ddcutil --display "$DISPLAY_NUM" getvcp 10 >/dev/null 2>&1; then + ddcutil --display "$DISPLAY_NUM" "$@" + return + fi + + if ddcutil --model "$MODEL" getvcp 10 >/dev/null 2>&1; then + ddcutil --model "$MODEL" "$@" + return + fi + + echo "External HG342PCB was not reachable via ddcutil on bus ${BUS_NUM}." >&2 + exit 1 +} + +run_ddcutil setvcp 10 "$delta" "$STEP" >/dev/null +``` + +### 2. `~/.local/bin/brightness-down-all` +```bash +#!/usr/bin/env bash +set -euo pipefail + +brightnessctl -q -d intel_backlight set 10%- +/home/jan/.local/bin/brightness-osd || true +``` + +### 3. `~/.local/bin/brightness-up-all` +```bash +#!/usr/bin/env bash +set -euo pipefail + +brightnessctl -q -d intel_backlight set +10% +/home/jan/.local/bin/brightness-osd || true +``` + +### 4. `~/.local/bin/brightness-osd` +```python +#!/usr/bin/env python3 +``` + +Dieses Skript zeigt ein kleines GTK4/libadwaita-OSD am oberen Bildschirmrand mit: +- Adwaita-Icon `display-brightness-symbolic` +- Prozentwert der internen Master-Helligkeit +- Fortschrittsbalken + +Das OSD ist einzeilig und kompakt. Wenn mehrere Tastendrücke schnell hintereinander kommen, wird der laufende OSD-Prozess aktualisiert und der Ausblend-Timer verlängert; dadurch verschwindet es nicht mehr zwischen zwei Helligkeitsänderungen. + +Für echte Positionierung am oberen Bildschirmrand unter GNOME Wayland wird dieses Fedora-Paket benötigt: + +```bash +sudo dnf install -y gtk4-layer-shell +``` + +Ohne `gtk4-layer-shell` darf GNOME/Wayland ein normales GTK-Fenster nicht zuverlässig oben positionieren; dann fällt das Skript auf die normale GTK-Fensterposition zurück. + +### 5. `~/.config/wluma/config.toml` +```toml +[als.iio] +path = "/sys/bus/iio/devices" +thresholds = { 0 = "night", 20 = "dark", 80 = "dim", 250 = "normal", 500 = "bright", 800 = "outdoors" } + +[[output.backlight]] +name = "eDP-1" +path = "/sys/class/backlight/intel_backlight" +capturer = "none" +``` + +### 6. `~/.local/bin/brightness-sync-hg342pcb` +```bash +#!/usr/bin/env bash +set -euo pipefail + +MODEL="${BRIGHTNESS_DDCUTIL_MODEL:-HG342PCB}" +DISPLAY_NUM="${BRIGHTNESS_DDCUTIL_DISPLAY:-1}" +BUS_NUM="${BRIGHTNESS_DDCUTIL_BUS:-16}" +BACKLIGHT_PATH="${BRIGHTNESS_SYNC_BACKLIGHT:-/sys/class/backlight/intel_backlight}" +INTERVAL="${BRIGHTNESS_SYNC_INTERVAL:-1}" +MIN_PERCENT="${BRIGHTNESS_SYNC_MIN_PERCENT:-1}" + +brightness_percent() { + local current max percent + + current="$(<"${BACKLIGHT_PATH}/brightness")" + max="$(<"${BACKLIGHT_PATH}/max_brightness")" + percent="$(( (current * 100 + max / 2) / max ))" + + if (( percent < MIN_PERCENT )); then + percent="$MIN_PERCENT" + elif (( percent > 100 )); then + percent=100 + fi + + printf '%s\n' "$percent" +} + +run_ddcutil() { + if sudo -n ddcutil --bus "$BUS_NUM" getvcp 10 >/dev/null 2>&1; then + sudo -n ddcutil --bus "$BUS_NUM" "$@" + return + fi + + if sudo -n ddcutil --display "$DISPLAY_NUM" getvcp 10 >/dev/null 2>&1; then + sudo -n ddcutil --display "$DISPLAY_NUM" "$@" + return + fi + + if sudo -n ddcutil --model "$MODEL" getvcp 10 >/dev/null 2>&1; then + sudo -n ddcutil --model "$MODEL" "$@" + return + fi + + echo "External ${MODEL} was not reachable via sudo -n ddcutil." >&2 + return 1 +} + +last_percent="" + +while true; do + percent="$(brightness_percent)" + + if [[ "$percent" != "$last_percent" ]]; then + if run_ddcutil setvcp 10 "$percent" >/dev/null; then + last_percent="$percent" + fi + fi + + sleep "$INTERVAL" +done +``` + +### 7. `~/.config/systemd/user/wluma.service` +```ini +[Unit] +Description=Adjust screen brightness automatically with wluma +PartOf=graphical-session.target +After=graphical-session.target + +[Service] +ExecStart=/usr/bin/wluma +Restart=always +PrivateNetwork=true +PrivateMounts=false + +[Install] +WantedBy=graphical-session.target +``` + +### 8. `~/.config/systemd/user/external-brightness-sync.service` +```ini +[Unit] +Description=Mirror internal panel brightness to external HG342PCB +PartOf=graphical-session.target +After=graphical-session.target wluma.service + +[Service] +ExecStart=%h/.local/bin/brightness-sync-hg342pcb +Restart=always +RestartSec=2 + +[Install] +WantedBy=graphical-session.target +``` + +--- + +## Dienste – finaler Soll-Zustand + +### Aktiv +```bash +systemctl --user status wluma.service +systemctl --user status external-brightness-sync.service +``` + +Erwartet: +- `wluma.service`: `active (running)` +- `external-brightness-sync.service`: `active (running)` + +### Nicht mehr Teil der Lösung +Diese Komponenten gehören **nicht** mehr zur finalen Lösung: + +- `ddcci-attach.service` +- `/usr/local/sbin/ddcci-attach-hg342pcb` +- `ddcci-refresh.service` +- `/usr/local/sbin/ddcci-refresh` +- `/etc/udev/rules.d/99-ddcci-hotplug.rules` + +Der Grund: +Mit dem Thunderbolt-Dock funktioniert die externe Steuerung sauber über `ddcutil`; die `ddcci`-Backlight-Variante wird dafür nicht mehr benötigt. + +--- + +## Cleanup alter / falscher Komponenten + +### Falls noch vorhanden: `ddcci-refresh` entfernen +```bash +sudo systemctl disable --now ddcci-refresh.service 2>/dev/null || true +sudo rm -f /etc/systemd/system/ddcci-refresh.service +sudo rm -f /usr/local/sbin/ddcci-refresh +sudo rm -f /etc/udev/rules.d/99-ddcci-hotplug.rules +sudo systemctl daemon-reload +sudo udevadm control --reload-rules +``` + +### Alte `ddcci`-Komponenten können entfernt werden +Die ältere `ddcci`-/`ddcci_backlight`-Variante ist **nicht mehr der finale Stand**. +Final ist jetzt die Hybrid-Variante über: + +- `intel_backlight` für intern +- `ddcutil` für extern + +--- + +## Bedienung im Alltag + +### Manuell beide Displays heller / dunkler +```bash +/home/jan/.local/bin/brightness-down-all +/home/jan/.local/bin/brightness-up-all +``` + +### Reaktionsverhalten +Das interne Display reagiert praktisch sofort. +Der externe Monitor reagiert typischerweise leicht verzögert, oft ungefähr nach einer Sekunde. + +Das ist bei `ddcutil` normal, weil zuerst DDC/CI-Kommunikation mit dem Monitor über das Dock stattfindet. +Etwas schneller könnte es eventuell mit `--noverify` werden, das ist aber weniger robust und daher aktuell nicht Teil des finalen Stands. + +### GNOME-Shortcuts +Wenn dafür Benutzerkürzel in GNOME gesetzt werden, immer den **vollen Pfad** verwenden: + +```text +/home/jan/.local/bin/brightness-down-all +/home/jan/.local/bin/brightness-up-all +``` + +Nicht `~/.local/bin/...`, weil das im GNOME-Shortcut-Feld unzuverlässig sein kann. + +--- + +## wluma und externer Sync – aktueller realer Zustand + +### Wichtig +`wluma` läuft automatisch nach dem Login und steuert nur das interne Display: + +```bash +systemctl --user status wluma.service +``` + +Der externe Monitor wird durch diesen User-Service nachgezogen: + +```bash +systemctl --user status external-brightness-sync.service +``` + +### Verhalten +`wluma` nutzt weiter den adaptiven Lernmodus, aber nur auf `intel_backlight`. +Der externe Sync-Dienst liest einmal pro Sekunde den internen Prozentwert aus: + +```text +/sys/class/backlight/intel_backlight/brightness +/sys/class/backlight/intel_backlight/max_brightness +``` + +und setzt denselben Prozentwert extern per: + +```bash +sudo -n ddcutil --bus 16 setvcp 10 +``` + +Das bedeutet: +- `wluma` entscheidet die Helligkeit nur einmal, am internen Panel +- extern folgt automatisch +- manuelle Änderungen über die gemeinsamen Skripte bleiben ebenfalls synchron +- bei einem Replug des externen Monitors kann ein `systemctl --user restart external-brightness-sync.service` sinnvoll sein + +### Praktische Folge +Wenn du möchtest, dass `wluma` besser wird, dann im Alltag in verschiedenen Lichtsituationen die Helligkeit mit deinen gemeinsamen Skripten nachregeln. Daraus lernt `wluma` die interne Master-Helligkeit; extern wird automatisch mitgezogen. + +--- + +## Sensorstatus + +Der Ambient-Light-Sensor funktioniert sauber. + +Nachgewiesen durch: +- `monitor-sensor` zeigt deutliche Lux-Änderungen +- `gsettings get org.gnome.settings-daemon.plugins.power ambient-enabled` war `true` +- `iio-sensor-proxy` läuft + +Das Problem lag daher **nicht** am Sensor, sondern an GNOME Auto Brightness. + +--- + +## Was bei Problemen sinnvoll ist + +### 1. Prüfen, ob intern und extern erreichbar sind +```bash +ls -l /sys/class/backlight +brightnessctl -l +sudo ddcutil --bus 16 getvcp 10 +``` + +### 2. Dienste prüfen +```bash +systemctl --user status wluma.service +systemctl --user status external-brightness-sync.service +``` + +### 3. Dienste neu starten +```bash +systemctl --user restart wluma.service +systemctl --user restart external-brightness-sync.service +``` + +--- + +## Was aktuell bewusst **nicht** gemacht wird + +- kein aggressives `delete_device` / `new_device` im laufenden Betrieb +- keine Hotplug-Automatik mit `ddcci-refresh` +- keine udev-Regel, die `ddcci` ständig neu erzeugt +- kein Verlassen auf GNOME Auto Brightness +- keine unabhängige `wluma`-Regelung für den externen Monitor + +Der Grund ist reine Stabilität: +Interner Master plus externer DDC/CI-Sync vermeidet Drift zwischen den Displays. + +--- + +## Kurzfazit + +### Final sauber gelöst +- intern läuft über `intel_backlight` +- extern läuft über `ddcutil` auf Bus 16 +- `wluma` läuft automatisch nach dem Login und steuert nur intern +- `external-brightness-sync.service` zieht den externen Monitor automatisch nach +- der ALS-Sensor funktioniert und wird von `wluma` genutzt + +### Final bewusst verworfen +- externes Display direkt von `wluma` regeln lassen +- `ddcci-refresh` +- Hotplug-Delete/Recreate-Automatik +- GNOME Auto Brightness als eigentliche Endlösung + +--- + +## Schnellbefehle + +### Status +```bash +systemctl --user status wluma.service +systemctl --user status external-brightness-sync.service +ls /sys/class/backlight +brightnessctl -l +sudo ddcutil --bus 16 getvcp 10 +``` + +### Manuelle gemeinsame Steuerung +```bash +/home/jan/.local/bin/brightness-down-all +/home/jan/.local/bin/brightness-up-all +``` + +### Dienste neu starten +```bash +systemctl --user restart wluma.service +systemctl --user restart external-brightness-sync.service +``` diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..ded9cf4 --- /dev/null +++ b/install.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +BACKLIGHT_DEVICE="${BRIGHTNESS_BACKLIGHT_DEVICE:-intel_backlight}" +BACKLIGHT_PATH="${BRIGHTNESS_SYNC_BACKLIGHT:-/sys/class/backlight/${BACKLIGHT_DEVICE}}" +DDC_MODEL="${BRIGHTNESS_DDCUTIL_MODEL:-HG342PCB}" +DDC_DISPLAY="${BRIGHTNESS_DDCUTIL_DISPLAY:-1}" +DDC_BUS="${BRIGHTNESS_DDCUTIL_BUS:-16}" +STEP="${BRIGHTNESS_STEP:-10}" +INTERVAL="${BRIGHTNESS_SYNC_INTERVAL:-1}" +MIN_PERCENT="${BRIGHTNESS_SYNC_MIN_PERCENT:-1}" + +install -d "$HOME/.local/bin" +install -d "$HOME/.config/systemd/user" +install -d "$HOME/.config/brightness-automation" + +install -m 0755 "$ROOT/bin/brightness-osd" "$HOME/.local/bin/brightness-osd" +install -m 0755 "$ROOT/bin/brightness-up-all" "$HOME/.local/bin/brightness-up-all" +install -m 0755 "$ROOT/bin/brightness-down-all" "$HOME/.local/bin/brightness-down-all" +install -m 0755 "$ROOT/bin/brightness-sync-hg342pcb" "$HOME/.local/bin/brightness-sync-hg342pcb" +install -m 0644 "$ROOT/systemd/user/external-brightness-sync.service" "$HOME/.config/systemd/user/external-brightness-sync.service" + +if [[ ! -f "$HOME/.config/brightness-automation/env" ]]; then + cat >"$HOME/.config/brightness-automation/env" </dev/null || true +rm -f "$HOME/.config/systemd/user/external-brightness-sync.service" +rm -f "$HOME/.local/bin/brightness-osd" +rm -f "$HOME/.local/bin/brightness-up-all" +rm -f "$HOME/.local/bin/brightness-down-all" +rm -f "$HOME/.local/bin/brightness-sync-hg342pcb" +systemctl --user daemon-reload + +cat <