Skip to content

Commit 041121c

Browse files
pid-fan-controller: init at 0.1.1, nixos/pid-fan-controller: init (#336849)
2 parents f266c5f + 16ca4e3 commit 041121c

File tree

3 files changed

+218
-0
lines changed

3 files changed

+218
-0
lines changed

nixos/modules/module-list.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,7 @@
651651
./services/hardware/nvidia-optimus.nix
652652
./services/hardware/openrgb.nix
653653
./services/hardware/pcscd.nix
654+
./services/hardware/pid-fan-controller.nix
654655
./services/hardware/pommed.nix
655656
./services/hardware/power-profiles-daemon.nix
656657
./services/hardware/powerstation.nix
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
{
2+
lib,
3+
config,
4+
pkgs,
5+
...
6+
}:
7+
let
8+
cfg = config.services.pid-fan-controller;
9+
heatSource = {
10+
options = {
11+
name = lib.mkOption {
12+
type = lib.types.uniq lib.types.nonEmptyStr;
13+
description = "Name of the heat source.";
14+
};
15+
wildcardPath = lib.mkOption {
16+
type = lib.types.nonEmptyStr;
17+
description = ''
18+
Path of the heat source's `hwmon` `temp_input` file.
19+
This path can contain multiple wildcards, but has to resolve to
20+
exactly one result.
21+
'';
22+
};
23+
pidParams = {
24+
setPoint = lib.mkOption {
25+
type = lib.types.ints.unsigned;
26+
description = "Set point of the controller in °C.";
27+
};
28+
P = lib.mkOption {
29+
description = "K_p of PID controller.";
30+
type = lib.types.float;
31+
};
32+
I = lib.mkOption {
33+
description = "K_i of PID controller.";
34+
type = lib.types.float;
35+
};
36+
D = lib.mkOption {
37+
description = "K_d of PID controller.";
38+
type = lib.types.float;
39+
};
40+
};
41+
};
42+
};
43+
44+
fan = {
45+
options = {
46+
wildcardPath = lib.mkOption {
47+
type = lib.types.str;
48+
description = ''
49+
Wildcard path of the `hwmon` `pwm` file.
50+
If the fans are not to be found in `/sys/class/hwmon/hwmon*` the corresponding
51+
kernel module (like `nct6775`) needs to be added to `boot.kernelModules`.
52+
See the [`hwmon` Documentation](https://www.kernel.org/doc/html/latest/hwmon/index.html).
53+
'';
54+
};
55+
minPwm = lib.mkOption {
56+
default = 0;
57+
type = lib.types.ints.u8;
58+
description = "Minimum PWM value.";
59+
};
60+
maxPwm = lib.mkOption {
61+
default = 255;
62+
type = lib.types.ints.u8;
63+
description = "Maximum PWM value.";
64+
};
65+
cutoff = lib.mkOption {
66+
default = false;
67+
type = lib.types.bool;
68+
description = "Whether to stop the fan when `minPwm` is reached.";
69+
};
70+
heatPressureSrcs = lib.mkOption {
71+
type = lib.types.nonEmptyListOf lib.types.str;
72+
description = "Heat pressure sources affected by the fan.";
73+
};
74+
};
75+
};
76+
in
77+
{
78+
options.services.pid-fan-controller = {
79+
enable = lib.mkEnableOption "the PID fan controller, which controls the configured fans by running a closed-loop PID control loop";
80+
package = lib.mkPackageOption pkgs "pid-fan-controller" { };
81+
settings = {
82+
interval = lib.mkOption {
83+
default = 500;
84+
type = lib.types.int;
85+
description = "Interval between controller cycles in milliseconds.";
86+
};
87+
heatSources = lib.mkOption {
88+
type = lib.types.listOf (lib.types.submodule heatSource);
89+
description = "List of heat sources to be monitored.";
90+
example = ''
91+
[
92+
{
93+
name = "cpu";
94+
wildcardPath = "/sys/devices/pci0000:00/0000:00:18.3/hwmon/hwmon*/temp1_input";
95+
pidParams = {
96+
setPoint = 60;
97+
P = -5.0e-3;
98+
I = -2.0e-3;
99+
D = -6.0e-3;
100+
};
101+
}
102+
];
103+
'';
104+
};
105+
fans = lib.mkOption {
106+
type = lib.types.listOf (lib.types.submodule fan);
107+
description = "List of fans to be controlled.";
108+
example = ''
109+
[
110+
{
111+
wildcardPath = "/sys/devices/platform/nct6775.2592/hwmon/hwmon*/pwm1";
112+
minPwm = 60;
113+
maxPwm = 255;
114+
heatPressureSrcs = [
115+
"cpu"
116+
"gpu"
117+
];
118+
}
119+
];
120+
'';
121+
};
122+
};
123+
};
124+
config = lib.mkIf cfg.enable {
125+
#map camel cased attrs into snake case for config
126+
environment.etc."pid-fan-settings.json".text = builtins.toJSON {
127+
interval = cfg.settings.interval;
128+
heat_srcs = map (heatSrc: {
129+
name = heatSrc.name;
130+
wildcard_path = heatSrc.wildcardPath;
131+
PID_params = {
132+
set_point = heatSrc.pidParams.setPoint;
133+
P = heatSrc.pidParams.P;
134+
I = heatSrc.pidParams.I;
135+
D = heatSrc.pidParams.D;
136+
};
137+
}) cfg.settings.heatSources;
138+
fans = map (fan: {
139+
wildcard_path = fan.wildcardPath;
140+
min_pwm = fan.minPwm;
141+
max_pwm = fan.maxPwm;
142+
cutoff = fan.cutoff;
143+
heat_pressure_srcs = fan.heatPressureSrcs;
144+
}) cfg.settings.fans;
145+
};
146+
147+
systemd.services.pid-fan-controller = {
148+
wantedBy = [ "multi-user.target" ];
149+
serviceConfig = {
150+
Type = "simple";
151+
ExecStart = [ (lib.getExe cfg.package) ];
152+
ExecStopPost = [ "${lib.getExe cfg.package} disable" ];
153+
Restart = "always";
154+
#This service needs to run as root to write to /sys.
155+
#therefore it should operate with the least amount of privileges needed
156+
ProtectHome = "yes";
157+
#strict is not possible as it needs /sys
158+
ProtectSystem = "full";
159+
ProtectProc = "invisible";
160+
PrivateNetwork = "yes";
161+
NoNewPrivileges = "yes";
162+
MemoryDenyWriteExecute = "yes";
163+
RestrictNamespaces = "~user pid net uts mnt";
164+
ProtectKernelModules = "yes";
165+
RestrictRealtime = "yes";
166+
SystemCallFilter = "@system-service";
167+
CapabilityBoundingSet = "~CAP_KILL CAP_WAKE_ALARM CAP_IPC_LOC CAP_BPF CAP_LINUX_IMMUTABLE CAP_BLOCK_SUSPEND CAP_MKNOD";
168+
};
169+
# restart unit if config changed
170+
restartTriggers = [ config.environment.etc."pid-fan-settings.json".source ];
171+
};
172+
#sleep hook to restart the service as it breaks otherwise
173+
systemd.services.pid-fan-controller-sleep = {
174+
before = [ "sleep.target" ];
175+
wantedBy = [ "sleep.target" ];
176+
unitConfig = {
177+
StopWhenUnneeded = "yes";
178+
};
179+
serviceConfig = {
180+
Type = "oneshot";
181+
RemainAfterExit = true;
182+
ExecStart = [ "systemctl stop pid-fan-controller.service" ];
183+
ExecStop = [ "systemctl restart pid-fan-controller.service" ];
184+
};
185+
};
186+
};
187+
meta.maintainers = with lib.maintainers; [ zimward ];
188+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
rustPlatform,
3+
fetchFromGitHub,
4+
lib,
5+
}:
6+
let
7+
version = "0.1.1";
8+
in
9+
rustPlatform.buildRustPackage {
10+
pname = "pid-fan-controller";
11+
inherit version;
12+
13+
src = fetchFromGitHub {
14+
owner = "zimward";
15+
repo = "pid-fan-controller";
16+
rev = version;
17+
hash = "sha256-ALR9Qa0AhcGyc3+7x5CEG/72+bJzhaEoIvQNL+QjldY=";
18+
};
19+
cargoHash = "sha256-Y57VSheI94b43SwNCDdFvcNxzkA16KObBvzZ6ywYAyU=";
20+
21+
meta = {
22+
description = "Service to provide closed-loop PID fan control";
23+
homepage = "https://github.com/zimward/pid-fan-controller";
24+
license = lib.licenses.gpl3Only;
25+
maintainers = with lib.maintainers; [ zimward ];
26+
platforms = lib.platforms.linux;
27+
mainProgram = "pid-fan-controller";
28+
};
29+
}

0 commit comments

Comments
 (0)