Skip to content

iurev/bebenqli

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

48 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

bebenqli

bebenqli logo

Control your BenQ RD280U from the terminal — a TUI and CLI over DDC/CI: brightness, color mode, volume, the Moon Halo backlight, eye-care modes, and a pile of undocumented BenQ features, without the OSD or BenQ's official app.



tests coverage 100% cognitive complexity ≤15 python 3.8+ license MIT

Built on ddcutil. Linux only. v0.0.5 — expect rough edges.

bebenqli TUI


Why

BenQ ships no public DDC/CI documentation for this panel. The VCP codes here were reverse-engineered by scraping the official Display Pilot 2 app's debug log — including the fact that the entire Moon Halo backlight is multiplexed into a single 16-bit register (d9 = (channel << 8) | value) instead of separate codes, with on/off living on a different register (d7). The TUI also has a built-in listen / discover mode (!) that watches VCP codes while you change settings on the monitor, to map new ones.

Requirements

  • Linux with i2c-dev (sudo modprobe i2c-dev)
  • ddcutil installed and on PATH
  • Your user in the i2c group: sudo usermod -aG i2c $USER (re-login after)
  • Python ≥ 3.8 + blessed (TUI only)
  • A BenQ RD280U connected via DisplayPort / HDMI / USB-C

Codes are specific to the RD280U. Other models will mostly not match.

Install

pipx (recommended — isolated, gives you the bebenqli command):

pipx install git+https://github.com/iurev/bebenqli

From a clone (no install, just run the package):

git clone https://github.com/iurev/bebenqli
cd bebenqli
pip install blessed          # only dependency
python -m bebenqli

ddcutil itself is a system package — install it from your distro (pacman -S ddcutil, apt install ddcutil, …); it is not bundled.

Usage

No arguments → launches the TUI. Any arguments → CLI mode.

TUI

bebenqli
Key Action
move between controls
change selected control
09 range: type a value · cycle: jump to that option
/ fuzzy-find a control
! listen / discover mode (map unknown VCP codes)
q quit

CLI

bebenqli list                  # every control, current value, range/options
bebenqli get volume            # -> 20
bebenqli set volume 30         # write THEN verify (read-back compare)
bebenqli set color-mode cinema # cycle: option name (case-insensitive) or raw value
bebenqli lazyset volume 25     # write only, no verify (fire-and-forget)
  • set checks ddcutil's exit code, then reads the value back and compares — prints (verified), (write-only, unverified), or MISMATCH… (exit 1).
  • Write-only controls (Moon Halo on/off, MH color temp) can't be read back.
  • lazyset just writes and exits 0 — use when you don't care to confirm.

idebug — interactive probe console

bebenqli idebug brightness     # focus one control, line-based REPL
bebenqli idebug kvm-switch     # unmapped control -> discovery mode

A round-trip (set then read-back) only proves the register stored what you wrote — not that the code controls the labelled feature, nor that anything visibly happened, nor whether one setting silently moves another. idebug puts a human + the monitor's OSD in the loop to settle that:

Command Does
set <val> / 50 / cinema set + verify read-back; prints Δ and what else changed
lazyset <val> write-only (fire-and-forget / unreadable controls)
get read current value (+ monitor-reported max)
watch poll this code while you turn the OSD — proves the code is right (Ctrl-C stops)
diff diff every readable code vs entry — reveals coupling
use <ctrl> switch focus (e.g. flip night-mode, then diff to see brightness move)
note <txt> record an observation · q quit (offers restore)

After each accepted write it asks visible change? [y/N/skip] — the only way to catch a register that stores a value but does nothing. Every action is logged to /tmp/benq/<control>.yaml as evidence.

Options

Flag Meaning
--bus N i2c bus override (default: auto-detect the RD280U)
-v --verbose echo each underlying ddcutil command to stderr
-V --version print version
-h --help usage

The i2c bus is assigned by the kernel per GPU + port, so it differs on every machine. bebenqli auto-detects it by matching the monitor model from ddcutil detect. Override with --bus N or $BEBENQLI_BUS.

VCP code map

Reverse-engineered from Display Pilot 2 logs. ✓ confirmed live · ~ flaky · ? guessed.

VCP Feature Values / notes Status
60 Input source 0f=DP, 11=HDMI, 13=USB-C
10 Brightness 0–100
12 Contrast 0–100
87 Sharpness 0–100
dc Color mode 30=Coding Dark, 31=Coding Light, 0f=M-Book, 32=Cinema, 1f=ePaper, 0a=sRGB, 12=User
14 Color preset 04=5000K, 05=6500K, 08=9300K, 0b=User
62 Volume 0–50 (0=silent; max is 50, not 100)
8d Mute flaky on fw 0.25 — use Volume=0 to silence instead ~
d9 Moon Halo (mux) (chan<<8)|value: chan 01=brightness 1–10, 07=color temp 1–10
d7 Moon Halo on/off 20=on, 10=off (write-only, needs --noverify)
d1 Night protection 0=off, 1=on, 2=auto
d0 Night level 1–10
19 Low blue light 0–5
fd Color weakness 00=off, 03=green-weak, 04=red-weak ?
e2 BI Gen2 auto-bright 00=off, FF=on
e5 BI Gen2 sensitivity 1–10
e7 Eco privacy 00=off
cc OSD language 02=en, 03=fr, 04=de, …
c9 Firmware version read-only

Moon Halo detail: it is not separate VCP codes. Write d9 = (channel << 8) | value; the high byte selects the sub-feature, the low byte is its value. Reads only ever return the brightness channel's low byte, so other sub-features are write-only. On/off is a separate register (d7).

Limitations / scope

  • RD280U only, Linux only, v0.0.5. No Windows/macOS, no other models.
  • Several codes are write-only (can't be read back to verify state).
  • Mute (8d) is unreliable on this firmware; use Volume=0.
  • Minimally maintained — issues/PRs welcome but responses may be slow.

Development

python3 -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"     # blessed + pytest + pytest-cov + flake8/pylint
pytest                       # runs the suite under a 100% coverage gate
flake8 bebenqli/             # readability gate: cognitive complexity ≤ 15

The monitor (and tmux) are faked behind a single run_proc seam, so the whole suite runs with no hardware and no ddcutil installed. The TUI layer is excluded from the coverage target (smoke-tested only); CLI and helpers are covered 100%.

The code is split into small single-purpose modules — proc (the shell-out seam), controls (the control table), format (value→string), ddc (the Ddc monitor connection), cli, tui, and app (entry/dispatch); __init__ is a thin facade. A flake8-cognitive-complexity gate keeps every function at or below a cognitive-complexity of 15, so nesting stays shallow and readable.

License

MIT © Vitalii Iurev

About

TUI + CLI to control BenQ RD280U monitors over DDC/CI (ddcutil) — brightness, color mode, volume, Moon Halo backlight, eye-care. Linux.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages