How to enable ddc brightness control? (i2c permissions)

I’ve been LOVING NixOS so far and have figured everything out except this LAST issue of being able to control my external monitor’s brightness from the computer. I know this is done with ddcutil and I’ve successfully got this working on Arch and PopOS, but i’m lost as to how to set this up in Nix.

This is the gnome extension i’d like to use, and I have it installed, along with ddcutil in my config.
https://extensions.gnome.org/extension/4652/adjust-display-brightness/

However, I think I need to load the kernel module and grant i2c non-root permissions? (this is where I get over my head)

This is my configuration file:

And this is how I got it to work on Arch… so I guess the question is, how can I accomplish this in Nix? Thank you in advance!


echo “i2c-dev” | sudo tee /etc/modules-load.d/i2c-dev.conf
sudo usermod $USER -aG i2c

ddc


Perhaps take a look here to see how it is done for the Plasma option services.xserver.desktopManager.plasma5.supportDDC.

1 Like

I have this in my config:

{ lib, config, pkgs, ... }: {
  boot.extraModulePackages = [config.boot.kernelPackages.ddcci-driver];
  boot.kernelModules = ["i2c-dev" "ddcci_backlight"];

  services.udev.extraRules = ''                                                                                                                                                                                       
    SUBSYSTEM=="i2c-dev", ACTION=="add",\                                                                                                                                                                             
      ATTR{name}=="NVIDIA i2c adapter*",\                                                                                                                                                                             
      TAG+="ddcci",\                                                                                                                                                                                                  
      TAG+="systemd",\                                                                                                                                                                                                
      ENV{SYSTEMD_WANTS}+="ddcci@$kernel.service"                                                                                                                                                                     
  '';

  systemd.services."ddcci@" = {
    scriptArgs = "%i";
    script = ''                                                                                                                                                                                                       
      echo Trying to attach ddcci to $1                                                                                                                                                                               
      i=0                                                                                                                                                                                                             
      id=$(echo $1 | cut -d "-" -f 2)                                                                                                                                                                                 
      if ${pkgs.ddcutil}/bin/ddcutil getvcp 10 -b $id; then                                                                                                                                                           
        echo ddcci 0x37 > /sys/bus/i2c/devices/$1/new_device                                                                                                                                                          
      fi                                                                                                                                                                                                              
    '';
    serviceConfig.Type = "oneshot";
  };

}

This makes the display brightness work the same as if it were a laptop backlight, so the brightness controls on my keyboard work. No extension required. The important part is the ddcci_backlight kernel module, which is supposed to be all you need, but due to a bug in Nvidia’s drivers, I had to add the udev rule and systemd service.

1 Like

Oh wow thank you. This looks promising. I’ll check this out! It’s the kernel extension and permissions that seem to be missing. This looks right.

  # DDC support
  boot.kernelModules = lib.optional cfg.supportDDC "i2c_dev";
  services.udev.extraRules = lib.optionalString cfg.supportDDC ''
    KERNEL=="i2c-[0-9]*", TAG+="uaccess"
  '';
1 Like

This is really interesting! I wonder how it works on a docked laptop that I sometimes need ddc when docked, but sometimes internal when used as a laptop

Ok, thanks to some digging AND feedback here, i’ve got it working on rebuild and boot. Basically just add these lines to the config. (one loads the kernel module, the other sets the udev rules)

  boot.kernelModules = ["i2c-dev"];
  services.udev.extraRules = ''
        KERNEL=="i2c-[0-9]*", GROUP="i2c", MODE="0660"
  '';

Then just make sure your user is added to the “i2c” group, like…

  users.users.mkelly = {
    isNormalUser = true;
    description = "Mike Kelly";
    extraGroups = [ "networkmanager" "wheel" "docker" "i2c"];
  };

When you boot up, you shoudl be able to run ddcutil commands without issue!

1 Like

I hit a similar issue a while back trying to get

running under NixOS; I knew the answer would be something with udev rules but (at the time) didn’t know how to look further, and then totally forgot about it.

So, I should revisit that. Thanks!

1 Like

Current most simplest NixOS way is to use hardware.i2c.enable. Posting here since this was showing up first in my Google results.

hardware.i2c.enable = true;
2 Likes

FWIW I have an AMD GPU now and this is all I need to have brightness controls via normal the backlight interface:

  boot.extraModulePackages = [config.boot.kernelPackages.ddcci-driver];
  boot.kernelModules = ["i2c-dev" "ddcci_backlight"];

I still have had to manually do echo ddcci 0x37 > /sys/bus/i2c/devices/$dev/new_device on rare occasion, but I no longer need to have it automatically run. I guess AMD’s driver just handles this a little bit better.

2 Likes

In theory the latter line is unnecessary if you’re logged in locally, according to the doc linked.

1 Like

Ah you are right, didn’t understand the “any user with a seat”. Thanks for pointing it out :slight_smile:

Note that this feature it’s actually broken, see `hardware.i2c.enable` incorrectly uses systemd's "uaccess" udev tag · Issue #210856 · NixOS/nixpkgs · GitHub.
The fix is pretty trivial but no one has done it, yet.

1 Like

That’s a good find, thanks for sharing. Though I just tested it with nixos-unstable and rebooted and it works as expected for me with just hardware.i2c.enable = true;

$ whoami
username
$ groups
users wheel video networkmanager scanner docker input qemu-libvirtd
$ ls -l /dev/i2c-7
crw-rw----+ 1 root i2c 89, 7 Mar 10 11:56 /dev/i2c-7
$ getfacl /dev/i2c-7
getfacl: Removing leading '/' from absolute path names
# file: dev/i2c-7
# owner: root
# group: i2c
user::rw-
user:username:rw-
group::rw-
mask::rw-
other::---

$ ddcutil detect
Display 1
   I2C bus:  /dev/i2c-7
   DRM connector:           card0-DP-1
   EDID synopsis:
      Mfg id:               DEL - Dell Inc.
      Model:                DELL xyz
      Product code:         xyz  (xyz)
      Serial number:        xyz
      Binary serial number: xyz (xyz)
      Manufacture year:     2020,  Week: 40
   VCP version:         2.1
1 Like

In addition to this with an AMD GPU I also needed

  services.udev.extraRules = ''
      KERNEL=="i2c-[0-9]*", GROUP="i2c", MODE="0660"
  '';

IIUC modifying services.udev.extraRules shouldn’t be necessary:

  • ddcutil includes a lib/udev/rules.d/60-ddcutil-i2c.rules file containing SUBSYSTEM=="i2c-dev", KERNEL=="i2c-[0-9]*", ATTRS{class}=="0x030000", TAG+="uaccess"
  • This file seems to be linked as /nix/var/nix/profiles/system/etc/profiles/per-user/USERNAME/lib/udev/rules.d/60-ddcutil-i2c.rules (I installed in users.users.<name>.packages), which I believe means it’s included in the udev rules automatically.

On the other hand, trying a command from this post, sudo udevadm test $(sudo udevadm info -q path -n i2c-7), listed it reading a lot of files, but not the ddcutil one…

It actually doesn’t. You have to add the package to services.udev.package for udev to use its rules

1 Like

OK, that seems like a bug. Not exactly easy to discover.

I don’t think I’d call it a bug… Like, we don’t include systemd units from a package unless it’s in systemd.packages. These are just things that should not be implicit.

If installing pkgs.ddcutil is not enough for that package to do its most common job, then I’d say it’s a bug.

This is why we very often have programs.foo.enable. To get it to do its thing correctly. environment.systemPackages literally just means “link things in /run/current-system/sw” and nothing else.

1 Like