1717# 6. Uses bash parameter expansion: ${var#pattern} and ${var%%pattern}
1818# 7. No temporary files or delays needed since fan2go is the only liquidctl user
1919#
20+ # Nix string literals:
21+ # https://nix.dev/manual/nix/2.28/language/string-literals.html#string-literals
22+ # https://nix.dev/manual/nix/2.28/language/string-interpolation.html
23+ # https://github.com/NixOS/nix/blob/master/doc/manual/source/language/string-interpolation.md
24+ #
2025# See also: https://github.com/arnarg/config/blob/8de65cf5f1649a4fe6893102120ede4363de9bfa/hosts/terra/fan2go.nix
2126#
2227#
3035# ├── Fan speed 5 0 rpm
3136# └── Fan speed 6 0 rpm
3237#
38+ # [das@l:~/nixos/desktop/l]$ liquidctl list -v
39+ # Device #0: Corsair Commander Core XT (broken)
40+ # ├── Vendor ID: 0x1b1c
41+ # ├── Product ID: 0x0c2a
42+ # ├── Release number: 0x0100
43+ # ├── Serial number: 210430f00a0857baae689a262091005f
44+ # ├── Bus: hid
45+ # ├── Address: /dev/hidraw5
46+ # └── Driver: CommanderCore
47+ #
3348{
3449 lib ,
3550 config ,
4055
4156 cfg = config . hardware . fan2go ;
4257
58+ # Path for the lock file to serialize access to liquidctl.
59+ liquidctlLockFile = "/run/lock/fan2go-liquidctl.lock" ;
60+
61+ # Vendor ID for the Corsair device to speed up liquidctl commands.
62+ liquidctlVendorId = "0x1b1c" ;
63+
64+ # Sleep duration between retries for liquidctl commands.
65+ retrySleepDuration = 0.1 ;
66+
67+ # Debug level for the scripts (0=off, 7=max debug).
68+ debugLevel = 7 ;
69+
70+ # A reusable bash helper function for logging.
71+ # It checks the DEBUG_LEVEL and prints messages to stderr.
72+ debugLogger = ''
73+ # Set default debug level if not provided.
74+ : "'' ${DEBUG_LEVEL:=0}" # Use quoted expansion to satisfy shellcheck
75+ : "'' ${DEBUG_LEVEL:=0}"
76+ LOG_FILE="/tmp/fan2go-debug-$(date +%Y%m%d%H).log"
77+ log_debug() {
78+ # Append message to the log file if debug level is 7 or higher.
79+ if [[ '' ${DEBUG_LEVEL} -ge 7 ]]; then echo "[$(date +%T)] DEBUG: $*" >> "$LOG_FILE"; fi
80+ if [[ '' ${DEBUG_LEVEL} -ge 7 ]]; then echo "[$(date +%T)] [$$] DEBUG: $*" >> "$LOG_FILE"; fi
81+ }
82+ '' ;
83+
4384 # Create the bash scripts for fan control
44- setPwmScript = pkgs . writeText "setPwm.bash" ''
45- #!${ pkgs . bash } /bin/bash
85+ setPwmScript = pkgs . writeShellApplication {
86+ name = "setPwm.bash" ;
87+ # A single, unified script to wrap all liquidctl interactions,
88+ # preventing race conditions by design.
89+ liquidctlWrapperScript = pkgs . writeShellApplication {
90+ name = "liquidctl-wrapper.bash" ;
91+ runtimeInputs = [ pkgs . liquidctl pkgs . util-linux pkgs . coreutils ] ;
92+ text = ''
4693 # Convert fan2go PWM (0-255) to liquidctl percentage (0-100)
47- percent=$((%pwm% * 100 / 255))
48- ${ pkgs . liquidctl } /bin/liquidctl set fan1 speed $percent
49- '' ;
94+ # PWM value is passed as the first argument
95+ ${ debugLogger }
96+ log_debug "setPwm started with argument: $1"
97+ ${ debugLogger }
98+ ACTION="$1"
99+ log_debug "Wrapper called with action: $ACTION"
50100
51- getPwmScript = pkgs . writeText "getPwm.bash" ''
52- #!${ pkgs . bash } /bin/bash
101+ # Check if the pwm_value argument was provided.
102+ : "'' ${1:?PWM value not provided as an argument}"
103+ percent=$(( $1 * 100 / 255 ))
104+ log_debug "Calculated percent: $percent"
105+ for i in {1..3}; do
106+ (
107+ log_debug "Attempt #$i: Acquiring lock and setting fan speed..."
108+ liquidctl --vendor ${ liquidctlVendorId } set fan1 speed "$percent" 2>> "$LOG_FILE"
109+ ) 200>${ liquidctlLockFile } && break
110+ log_debug "Attempt #$i failed. Sleeping for ${ toString retrySleepDuration } s."
111+ sleep ${ toString retrySleepDuration }
112+ done
113+ '' ;
114+ } ;
115+ case "$ACTION" in
116+ set-pwm )
117+ PWM_VALUE = "$2"
118+ : "''${ PWM_VALUE :?PWM value not provided for set-pwm action } "
119+ percent = $( ( PWM_VALUE * 100 / 255 ) )
120+ log_debug "Calculated percent: $percent"
121+ for i in { 1. .3 } ; do
122+ ( flock 200 ; liquidctl --vendor ${ liquidctlVendorId } set fan1 speed "$percent" 2 >> "$LOG_FILE" ) 200 >${liquidctlLockFile } && break
123+ log_debug "Attempt #$i failed. Sleeping."
124+ sleep ${ toString retrySleepDuration }
125+ done
126+ ; ;
127+ get-pwm |get-rpm )
128+ output = ""
129+ for i in { 1. .3 } ; do
130+ output = $( ( flock - s 200 ; liquidctl - - vendor ${ liquidctlVendorId } status 2 >> "$LOG_FILE" ) 200 >${liquidctlLockFile } )
131+ [ -n "$output" ] && break
132+ log_debug "Attempt #$i failed (no output). Sleeping."
133+ sleep ${ toString retrySleepDuration }
134+ done
135+ log_debug "Raw liquidctl output: $output"
136+
137+ getPwmScript = pkgs . writeShellApplication {
138+ name = "getPwm.bash" ;
139+ runtimeInputs = [ pkgs . liquidctl pkgs . util-linux pkgs . coreutils ] ;
140+ text = ''
53141 # Get current fan RPM and convert to PWM value
54- output=$(${ pkgs . liquidctl } /bin/liquidctl status 2>/dev/null)
55- if [[ $output =~ Fan\ speed\ 1[^0-9]+([0-9]+) ]]; then
56- rpm=${ BASH_REMATCH [ 1 ] }
142+ output=""
143+ ${ debugLogger }
144+ log_debug "getPwm started."
145+
146+ for i in {1..3}; do
147+ output=$( (
148+ log_debug "Attempt #$i: Acquiring lock and getting status..."
149+ flock -s 200 # Use a shared lock for read-only operations
150+ liquidctl --vendor ${ liquidctlVendorId } status 2>> "$LOG_FILE"
151+ ) 200>${ liquidctlLockFile } )
152+ [ -n "$output" ] && break
153+ log_debug "Attempt #$i failed (no output). Sleeping for ${ toString retrySleepDuration } s."
154+ sleep ${ toString retrySleepDuration }
155+ done
156+ log_debug "Raw liquidctl output: $output"
157+ if [[ $output =~ Fan\ speed\ 1[^0-9]+([0-9]+) ]]; then
158+ rpm='' ${BASH_REMATCH[1]}
57159 echo $((rpm * 255 / 2000))
58- else
59- echo 0
160+ exit 0
60161 fi
61- '' ;
162+ echo 0
163+ if [[ $output =~ Fan\ speed\ 1[^0-9]+([0-9]+) ]]; then
164+ rpm='' ${BASH_REMATCH[1]}
165+ if [[ "$ACTION" == "get-pwm" ]]; then
166+ echo $((rpm * 255 / 2000))
167+ else # get-rpm
168+ echo "$rpm"
169+ fi
170+ else
171+ echo 0
172+ fi
173+ ;;
174+ *)
175+ log_debug "Unknown action: $ACTION"
176+ exit 1
177+ ;;
178+ esac
179+ '' ;
180+ } ;
62181
63- getRpmScript = pkgs . writeText "getRpm.bash" ''
64- #!${ pkgs . bash } /bin/bash
182+ getRpmScript = pkgs . writeShellApplication {
183+ name = "getRpm.bash" ;
184+ runtimeInputs = [ pkgs . liquidctl pkgs . util-linux pkgs . coreutils ] ;
185+ text = ''
65186 # Get current fan RPM value
66- output=$(${ pkgs . liquidctl } /bin/liquidctl status 2>/dev/null)
67- if [[ $output =~ Fan\ speed\ 1[^0-9]+([0-9]+) ]]; then
68- rpm=${ BASH_REMATCH [ 1 ] }
69- echo $rpm
70- else
71- echo 0
187+ output=""
188+ ${ debugLogger }
189+ log_debug "getRpm started."
190+
191+ for i in {1..3}; do
192+ output=$( (
193+ log_debug "Attempt #$i: Acquiring lock and getting status..."
194+ flock -s 200 # Use a shared lock for read-only operations
195+ liquidctl --vendor ${ liquidctlVendorId } status 2>> "$LOG_FILE"
196+ ) 200>${ liquidctlLockFile } )
197+ [ -n "$output" ] && break
198+ log_debug "Attempt #$i failed (no output). Sleeping for ${ toString retrySleepDuration } s."
199+ sleep ${ toString retrySleepDuration }
200+ done
201+ log_debug "Raw liquidctl output: $output"
202+ if [[ $output =~ Fan\ speed\ 1[^0-9]+([0-9]+) ]]; then
203+ rpm='' ${BASH_REMATCH[1]}
204+ echo "$rpm"
205+ exit 0
72206 fi
73- '' ;
207+ echo 0
208+ '' ;
209+ } ;
74210
75211 # Create a shellcheck validation script
76- shellcheckScript = pkgs . writeText "check-fan-scripts.sh" ''
77- #!${ pkgs . bash } /bin/bash
212+ shellcheckScript = pkgs . writeShellApplication {
213+ name = "check-fan-scripts.sh" ;
214+ runtimeInputs = [ pkgs . shellcheck ] ;
215+ text = ''
78216 # Shellcheck validation for fan control scripts
79217 echo "Running shellcheck on fan control scripts..."
80218
81219 echo "Checking setPwm script..."
82- ${ pkgs . shellcheck } /bin/shellcheck ${ setPwmScript } || exit 1
220+ shellcheck ${ setPwmScript } /bin/setPwm.bash || exit 1
221+ shellcheck ${ liquidctlWrapperScript } /bin/liquidctl-wrapper.bash || exit 1
83222
84223 echo "Checking getPwm script..."
85- ${ pkgs . shellcheck } /bin/shellcheck ${ getPwmScript } || exit 1
224+ shellcheck ${ getPwmScript } /bin/getPwm.bash || exit 1
225+ shellcheck ${ liquidctlWrapperScript } /bin/liquidctl-wrapper.bash || exit 1
86226
87227 echo "Checking getRpm script..."
88- ${ pkgs . shellcheck } /bin/shellcheck ${ getRpmScript } || exit 1
228+ shellcheck ${ getRpmScript } /bin/getRpm.bash || exit 1
229+ shellcheck ${ liquidctlWrapperScript } /bin/liquidctl-wrapper.bash || exit 1
89230
90231 echo "All scripts passed shellcheck validation!"
91- '' ;
232+ '' ;
233+ } ;
92234
93235 fan2goConfig = pkgs . writeText "fan2go.yaml" ''
94236 #
@@ -107,15 +249,28 @@ let
107249 # We use a shell command to convert the 0-255 PWM value from fan2go
108250 # into a 0-100 percentage for liquidctl.
109251 setPwm:
110- exec: "${ setPwmScript } "
252+ exec: "${ setPwmScript } /bin/setPwm.bash"
253+ args: ["%pwm%"]
254+ exec: "${ liquidctlWrapperScript } /bin/liquidctl-wrapper.bash"
255+ args: ["set-pwm", "%pwm%"]
256+ env:
257+ DEBUG_LEVEL: "${ toString debugLevel } "
111258 # The `getPwm` command should return the current PWM value.
112259 # Since liquidctl doesn't provide PWM directly, we convert from the RPM value.
113260 getPwm:
114- exec: "${ getPwmScript } "
261+ exec: "${ getPwmScript } /bin/getPwm.bash"
262+ exec: "${ liquidctlWrapperScript } /bin/liquidctl-wrapper.bash"
263+ args: ["get-pwm"]
264+ env:
265+ DEBUG_LEVEL: "${ toString debugLevel } "
115266 # The `getRpm` command gets the current RPM value from liquidctl.
116267 # This helps fan2go understand the fan's current state.
117268 getRpm:
118- exec: "${ getRpmScript } "
269+ exec: "${ getRpmScript } /bin/getRpm.bash"
270+ exec: "${ liquidctlWrapperScript } /bin/liquidctl-wrapper.bash"
271+ args: ["get-rpm"]
272+ env:
273+ DEBUG_LEVEL: "${ toString debugLevel } "
119274 # Fan speed is a percentage for liquidctl
120275 min: 10
121276 max: 100
211366 after = [ "lm_sensors.service" ] ;
212367
213368 serviceConfig = {
214- ExecStartPre = "${ shellcheckScript } " ;
369+ ExecStartPre = "${ shellcheckScript } /bin/check-fan-scripts.sh " ;
215370 ExecStart = lib . concatStringsSep " " [
216371 "${ pkgs . fan2go } /bin/fan2go"
217372 "-c"
218373 "${ fan2goConfig } "
219374 "--no-style"
220375 ] ;
221376
377+ Environment = [ "GOMEMLIMIT=45MiB" ] ;
222378 MemoryHigh = "48M" ;
223379 MemoryMax = "64M" ;
224380 CPUQuota = "50%" ;
0 commit comments